20 世紀 80 年代末出現的很多數字指示板更像是汽車的儀錶盤,很不精緻,或者說相當粗糙。其中更是少有能夠以一種吸引人的方式展示業務數據的。如今,基於 Web 的指示板能夠達到這樣的目的。通過本文,了解一個好的指示板是什麼樣的以及如何識別並利用關鍵績效指標(KPI)來製作更為有效的數字指示板。最後,使用 eXist XML 資料庫和 XQuery 構建一個 Web 指示板。
幾年前,我為一個客戶製作了大量與體育比賽有關的數據提要。我們需要尋求一種方法來體現此客戶的數據服務產品的廣度和深度,我們於是決定設計一個類似記分板似的東西放在客戶網站上。我們當時的想法是通過顯示關鍵的體育比賽數據,比如 “足球比賽得分”、“賽馬” 或 “板球比賽的次數” — 所有實時記錄的數據 — 來達到理想的效果並將此客戶數據服務的質量有效地傳達給潛在的顧客。
至於這個特定的記分板如何開發並非本文的目標:我之所以重新把它拿出來是因為在這個小部件部署后發生了一件特殊的事情。
|
這個記分板投入使用后,我的這個客戶的高級管理層開始就所生成的數據提出問題。最初,這些問題圍繞著數據合計的準確性,隨後的問題愈發發人深省,比如 “我們應該顯示更多的足球得分” 或 “為何賽事多的時候更新速度要比平常的時候慢”。我們所生成的各種數字最終成為了衡量此客戶與體育比賽相關的數據提要的質量和總體狀況的有效基準數據。
由於這個計分板應用程序差不多每秒要更新一次,這樣就將此公司的運轉情況及時地傳遞給了高級管理層以供其快速地發現任何不正常之處。當然,我們所做的沒什麼新鮮的 — 只不過是開發 KPI 指示板理念的再現 — 但是創建它時加入了一些市場意識。
指示板概覽
在企業開發界,一段時期內,指示板曾經是人們大肆宣揚的一個主題,所以讓我先來給出指示板的一個非正式的定義,不過只限在本文中使用:
指示板 指的是實現一個或多個目標所需的重要信息的可視展示,這些信息經過整理並被恰當地擺放在屏幕上以便於人們一眼就能理解這些信息。
數據的目標和類型可以分為如下三個類別:
除了上述這些類別,我所遇到或見過的指示板還常常具有一些共性,比如它們多久更新一次,它們展示的是定量數據、定性數據還是二者都展示 — 有一些頗值得我們進行細緻研究。
|
好的 KPI 是什麼樣的?
指示板傾向於顯示可度量的一些指標,在商界通常就是指所謂的關鍵績效指標(KPI)。一個 KPI 指的是能表明公司業務處理狀況的任何可測指標。因此,一個好的 KPI 應該能夠很好地度量公司內的任何戰略、分析或操作目標的成功或失敗。
側欄 “KPI 的例子” 給出了您的公司可以採用的幾個 KPI 例子。實際上,這些 KPI 對很多公司都是適用的:我鼓勵尋找(正如在之前的記分板示例中那樣)能簡便地展現業務處理狀況特徵的一個 KPI。KPI 越是接近您公司的具體業務,指標就越合適。
請務必注意 KPI 的定義有可能會不斷變化,因為隨著時間和經驗的積累您會掌握更合適的定義它們的方法,以及用它們所想要回答的問題。畢竟,使用 KPI 的最終目的還是提出並解答有關您業務情況的這些問題。
除此之外,無須害怕 KPI:它可以很高深,也可以很簡單,這取決於您的選擇,而且現在 Web 上有大量的參考資源可幫助您打造出自己的 KPI。我認為在創建 KPI 過程中最重要的一件事情就是確保有數據可用。
好的指示板是什麼樣的?
一個好的 Web 指示板總是力求以最簡明的方式呈現這些 KPI,它最大化數據,並儘可能地減少與信息表達無關的圖形 “噪音”。按照戰略、操作和分析目標來組織這些顯示元素是個很好的做法。在對想要進行對比的條目進行分組時,需要檢查對比是否相關、有效並且這個對比提供了對業務的一些有用反饋,而不是解釋過去發生的事情。雖然解釋過去發生的某個事情可以算是一種服務,但是指示板的任務是給出事情現在和當前的狀態。如果需要歷史記錄來更好地了解現狀,可以讓所構建的指示板或其數據能夠定期地被保存。
若一個指示板在一開始就以最為簡要的格式顯示數據,而後又大肆顯示更多的信息,它很容易就會變成一個瀏覽信息的工具。即便您的確提供了像數據向下鑽取或導航 UI 之類的元素,還是需要將此指示板與特定的用戶角色相匹配。最佳的做法是讓一個指示板對應於一個特定的角色。
最後,一個好的 Web 指示板會避開過於精細的圖片。典型的例子是使用類似三維 (3D) 餅圖這樣的圖形,其實,額外的深度反倒會讓可視地決定各個塊在整個圖形中所佔的比例變得更為困難。更甚者,現在,已經有很多討論(更多信息,請參見 參考資料)共同反對使用這種餅圖。
如果與我上面剛剛給出的這些建議背道而馳,那麼得到的就是一個糟糕的指示板。這裡,我列出了在設計得很糟糕的指示板應用程序中常見的幾個顯著特徵:
數據墨水(data ink)的基本理念蘊含著描述指示板好壞的另一種方式。作為信息和數據設計的一個基本原則,數據墨水 實際上是被列印頁面上代表數據(比如,數值或圖片上的數據線)的任何內容所用到的所有的墨水。計算機屏幕也是同樣的道理,其中數據像素類似於數據墨水,而其他的事情則不是。最大化信息設計內的數據像素意味著需要刪除盡量多的無關的圖形元素、使用低調的顏色模式並避免花哨的字體。
安裝示例指示板
在我向您展示如何構建這個示例指示板前,可以從 下載 部分獲得源文件並安裝這個指示板。為此,解壓縮 .zip 文件並遵循其中所包含的 README 文件的指導。
在安裝完畢后,應該能夠使用一個 Web 瀏覽器和如下的 URL 從 eXist 資料庫訪問 dashboard.xq 來查看這個示例指示板:
http://localhost:8080/exist/rest/db/dashboards_with_xquery/xquery/dashboard.xq |
如果 XQuery 文件安裝在 eXist 的其他地方,可以相應調整這個 URL。
同樣應該注意這個示例指示板使用了很多測試數據和在線 Web 服務的憑證,所有這些均存在於 Internet 上。這些資源應該還可用,但是如果您遇到任何問題,我建議您用您自己的憑證和測試數據代替示例數據。
構建一個公司指示板
了解了指示板的原理,也知道了其主要目標就是以一種動人的方式展示 KPI 來幫助您提出並解答重要的業務問題,現在就可以開始創建這個示例指示板了。使用像 eXist XML 資料庫和 XQuery 這類 XML 技術就可以聚集和使用 XML 數據,然後再創建指示板的一個 HTML 表示。
圖 1 顯示了這個示例指示板的最終外觀。
這個 Web 頁面少有新奇的設計元素來吸引眼球,這是我遵循前面所提到的有關最大化數據像素建議的結果。這個 CSS 文件定義了一個顏色一般的背景色和一種很便於閱讀的字體。
我選擇了幾個現成的 KPI 來展示 XQuery 能夠多麼好地集成數據:
指示板不太像是一個十分成熟的應用程序,它充分利用了 XQuery 的 eXist XML 資料庫實現中的特定功能。我們將要創建的這個指示板僅使用三個 XQuery 文件就可以實現,其中一個是供其他兩個文件使用的 XQuery 模塊,參見 圖 2。
utility.xqm XQuery 模塊包含 data.xq 與 dashboard.xq 所使用的一些函數。data.xq XQuery 文件的作用是生成一個包含所有與 KPI 相關的數據的 XML 文檔。dashboard.xq 則用來顯示數據,創建一個可以用瀏覽器查看的 HTML 文件。
首先是數據源
當我開發軟體時,首先要做的就是充實應用程序的數據層:如果一個有用的應用程序不 依靠數據,未免太不正常。在創建一個用來展示數據的指示板時,首先要做的自然就是確定數據源。
指示板的目標就是顯示 KPI,所以需要集成數據源,這些數據源可以是 KPI 本身,或者是與其計算相關的其他數據。不管 KPI 本質上是定性的還是定量的,所有的 KPI 都需要能夠展現出一些有關業務運轉的有用信息。以這個指示板示例為例,我使用了公開可用的數據來說明 XQuery 是如何處理集成場景的。此外,我還選用了與在內部、外部及業務周邊生成的 XML 數據有關的一系列流行的 Web 服務。
圖 3 中的圖表顯示了一組數據源,它們可以從 Google™ 之類流行的 Web 服務得到,也可以通過諸如關係資料庫這樣的常見技術獲得。
每種數據源都需要與一個 Web 服務、XML 數據或 XQuery 生成的數據相集成:
data.xq 的處理結果應是包含了所有所需數據的一個 XML 文檔。在 XQuery 內進行數據集成的最簡單的一個方法就是使用 doc() 函數,它可以從 Web URL 指向的站點檢索 XML。如果這個 URL 指向的是一個返回的數據結構不合法(從 XML 的角度來說)的資源,這時候再使用此函數就會得到一個錯誤。
清單 1 顯示了 data.xq 中的處理過程。
<sales_pipeline desc="example accessing published Google spreadsheet"> {doc("http://spreadsheets.google.com/feeds/list/ pZDPqHJcLzxKntsQv2tuIMQ/1/public/basic")} </sales_pipeline> ... <backpack desc="example accessing simple web service"> {doc("http://dashboardwithxquery.backpackit.com/ 69babcbce8aa212fc83464088b45f7a97a9f3dce/reminders.xml")} </backpack> ... <timesheet desc="example accessing local MS Excel file"> doc("file:///Users/jimfuller/Source/Writing/1_articles/1_ibm/ 3_dashboards_with_xquery/working/src/data/MSOFFICE-timesheet.xls") </timesheet> |
sales_pipeline 元素訪問我發布的並可通過 URL 訪問的一個 Google 文檔。用這種方式,Google 文檔就可以作為 Atom XML 提要(相關鏈接,參見 參考資料)公開。Backpack URL 返回一個 XML 提要,它代表的是我在 URL http://www.backpackit.com 處設置的提醒(reminder)。timesheet 元素則不同,它負責訪問一個本地的 Excel 文件。幾年前,使用以特定於 Microsoft 的 XML 格式生成的 Excel 電子數據表也是可以的。
數據集成的下一個步驟就是使用 XQuery 創建一套幫助函數,它們均位於 utility.xqm XQuery 模塊中。以下是對這些函數的一個簡短介紹,並突出顯示了相關代碼:
declare function utility:check_site($uri) { let $start := util:system-time() let $response := httpclient:get(xs:anyURI($uri),false(),()) let $end := util:system-time() let $response-time := (($end - $start) div xs:dayTimeDuration('PT1S')) * 1000 let $status-code := string($response/@statusCode) return <test xmlns="" ts="{current-dateTime()}" status="{$status-code}" response="{$response-time}" /> }; |
此函數返回的是一個測試元素,dashboard.xq 使用這個測試元素來顯示 Web 站點的狀態。
declare function utility:get-aged-invoices(){ let $connection := sql:get-connection("com.mysql.jdbc.Driver", "jdbc:mysql://localhost/test", "root", "") let $data := sql:execute($connection, "select * from invoices;", fn:true()) return <invoices> <total>{sum($data/sql:row/sql:invoice_amount)}</total> <age amt="15"> {sum($data/sql:row/sql:invoice_amount[../sql:invoice_terms='15'])}</age> <age amt="30"> {sum($data/sql:row/sql:invoice_amount[../sql:invoice_terms='30'])}</age> <age amt="60"> {sum($data/sql:row/sql:invoice_amount[../sql:invoice_terms='60'])}</age> </invoices> }; |
此函數先是使用 eXist SQL 擴展模塊的 sql:get-connection() 函數建立一個 Java™ Database Connectivity (JDBC)-風格的連接。然後,定義一個 SQL 語句來從這個資料庫選擇數據,此資料庫用 sql:execute 執行。此函數的結果保存在 $data 變數內,之後再使用它來通過 XPath 挑選相關數據。
如下所列的是一系列與各種 Google 的 Web 服務相集成的函數(更多信息,請參見 參考資料)。Google 的很多 Web 服務使用類似的技術來公開和連接它們,所以我特意給出了其中的幾個例子。(這些例子並沒有用在我們的這個示例指示板中,但它們可能對您很有啟發)。
declare function utility:get-google-token($Email,$Passwd,$accountType,$source,$service){ let $params := concat("Email=",$Email, "&Passwd=",$Passwd, "&source=",$source, "&accountType=",$accountType, "&service=",$service) let $uri := concat("https://www.google.com/accounts/ClientLogin?",$params) let $response := httpclient:get(xs:anyURI($uri),false(),()) let $token := substring-after(xmldb:decode($response),"Auth=") return string($token) }; |
請注意此函數返回的是一個字元串,而非 XML。在 XQuery,可以限制函數的輸入和輸出並給它們賦予一個數據類型。
declare function utility:get-google-spreadsheet-feed($Email,$Passwd){ let $accountType := "HOSTED_OR_GOOGLE" let $source := "Dashboards-XQUERY-Example" let $service := "wise" let $token := utility:get-google-token($Email,$Passwd,$accountType,$source,$service) let $headers := <headers> <header name="Authorization" value="GoogleLogin auth={$token}"/> </headers> let $uri := xs:anyURI('http://spreadsheets.google.com/feeds/spreadsheets/private/full') return httpclient:get($uri, false(), $headers) }; |
此函數製作了一個恰當的 HTTP 請求來檢索 Google 文檔內的文檔列表,這之後可被用來打開這些文檔。
declare function utility:get-google-atom-feed($user,$pass,$label){ let $token := util:string-to-binary(concat($user,':',$pass)) let $headers := <headers> <header name="Authorization" value="Basic {$token}"/> </headers> let $uri := xs:anyURI(concat('https://mail.google.com/mail/feed/atom/',$label)) return httpclient:get($uri, false(), $headers) }; |
declare function utility:get-google-cal-feed($Email,$Passwd){ let $accountType := "HOSTED_OR_GOOGLE" let $source := "Dashboards-XQUERY-Example" let $service := "cl" let $token := utility:get-google-token($Email,$Passwd,$accountType,$source,$service) let $headers := <headers> <header name="GData-Version" value="2"/> <header name="Authorization" value="GoogleLogin auth={$token}"/> </headers> let $uri := xs:anyURI( concat('http://www.google.com/calendar/feeds/', string($Email), '/private/full') ) return httpclient:get($uri, false(), $headers) }; |
這些函數被 data.xq 用來創建一個大型的 XML 文檔,該文檔就是這個數字指示板的源文檔。如果您已經安裝了代碼,我建議您通過瀏覽器訪問 data.xq,您應該能夠看到類似 圖 4 的結果。
這個 markup 的結構是任意的,您可以自由選擇其他的方式。
指示板的表示
dashboard.xq XQuery 文件所生成的結果 — 一個 HTML 文檔 — 基於特定於 eXist XML 資料庫的序列化選項來表示這個指示板:
declare option exist:serialize "method=xhtml media-type=text/html indent=yes omit-xml-declaration=no"; |
要創建這個示例指示板,用聚合后的這個 XML 文檔和 utility.xqm 內定義的這些 XQuery 函數構造這個 Web 頁面的各個特定區域,如 圖 5 所示。
|
KPI 是混合使用條形圖和 HTML 來顯示的,以給出特定指標的狀態。在使用條形圖的區域,我選擇了部署這個 Google Charting API。這個 API 創建了在 sales pipeline、aging invoices 和 time sheets 區域內用到的那些圖表:
為了便於創建這些圖表,我創建了一個 XQuery 函數 utility:graph(),此函數在 utility.xqm 內定義(參見 清單 8)。
(: Wrap up Google Charting API :) declare function utility:graph($type,$colors,$size,$markers,$data,$alt){ let $src :=concat('http://chart.apis.google.com/chart?chf=bg,s,F7F5E6&chco=',$colors, '&chs=',$size, '&cht=',$type, '&chl=',$markers, '&chd=t:',$data ) return <img alt="{$alt}"src="{$src}"/> }; |
utility:graph() 函數構造這個 URL,返回結果為 img 元素。有關 Google Charting API 選項的更多信息,請參見 參考資料。
這個示例指示板需要數據才能完成特定的功能,所以接受了使用 doc() 函數由 data.xq 生成的聚合數據,並將結果保存在 $data 變數內:
let $data := doc('http://localhost:8080/exist/rest/db/dashboards_with_xquery/xquery/data.xq') |
將這個 XML 文檔放入 $data 變數意味著這些指標全部是同時測量的。這雖然不是很必要,但是它意味著所有的指標都是同步的 — 如希望定期保存結果和查看歷史性能的話,這一點十分有用。
隨後的清單描述了每個顯示區域並解釋了 XQuery 的主要工作原理。我用粗體 突出顯示了 XQuery 代碼以便讓其能夠從 HTML 標記中明顯地區分出來。
Aging Invoices
清單 9 中的 XQuery 代碼顯示了三個條形圖,但它們必須要統一以便於比較。由於所使用的範圍是 10,000,這意味著除數值也是這個值,然後再乘以 100 獲得在條形圖中所用的百分比。
<h3>Aging Invoices<sup>(mysql)</sup></h3> <table> <tr> <td>£{$data/data/strategic/financial/invoices/age[@amt='15']}</td> <td> { let $amt :=(xs:float($data/data/strategic/financial/invoices/age[@amt='15']) div 10000) * 100 return utility:graph("bhs","4D89F9", "150x30", "0|10000",$amt, '15 day') } </td> <td>15 Day</td> </tr> <tr> <td>£{$data/data/strategic/financial/invoices/age[@amt='30']}</td> <td>{ let $amt := (xs:float($data/data/strategic/financial/invoices/age[@amt='30']) div 10000) * 100 return utility:graph("bhs", "4D89F9", "150x30", "0|10000", $amt, '30 day') } </td> <td>30 Day</td> </tr> <tr> <td>£{$data/data/strategic/financial/invoices/age[@amt='60']}</td> <td>{ let $amt := (xs:float($data/data/strategic/financial/invoices/age[@amt='60']) div 10000) * 100 return utility:graph("bhs","4D89F9", "150x30", "0|10000", $amt, '60 day') } </td> <td>60 Day</td> </tr> </table> |
每個錶行都需要選擇正確的數據,這可以通過選擇 amt 屬性實現,該屬性代表的是總的數量。請記住此數據最初從 MySQL 資料庫生成,所以需要確保 MySQL 伺服器運行並由示例數據集載入。此外,還需要取消註釋這個 data.xq 代碼,正如 README 安裝指導文件內解釋的那樣。
Sales Pipeline
sales pipeline 數據從一個可公開查看的 Google Docs 電子數據表獲得。這意味著數據是從一個 Atom 提要中選擇 得到的。在呈現數據時惟一一個有些奇怪的地方是這個 Atom 提要(來自 Google 電子數據表)按行提供的數據內包含分隔符,如 清單 10 所示。
<h3>Sales Pipeline <sup>(google spreadsheet)</sup></h3> <table> {for $item in $data/data/strategic/sales_pipeline/atom:feed/atom:entry/atom:content return let $cols := tokenize($item,',') return <tr> <td>£{substring-after($cols[2],':')}</td> <td>{ let $amt := (xs:float(substring-after($cols[2],':')) div 10000) * 100 return utility:graph("bhs", "4D89F9,C6D9FD", "150x30", "0|10000", $amt, substring-after($cols[1],':')) }</td> <td>{ substring-after($cols[1],':') }</td> </tr> } </table> |
XML 內包含分隔了的數據多少有些怪異,但 XQuery 提供了很多解析它的方法。我使用了 tokenize() 函數來分隔每列,然後再用 substring-after() 函數獲得這個數據值。
Weekly Timesheets
有的時候,獲得 timesheet 數據最好的方式就是打開一個本地的電子數據表。現在,Excel 已經支持 XML 格式,所以只需知道其結構就可以開始使用它了,如 清單 11 所示。
<h3>Weekly Timesheets <sup>(MS Excel)</sup></h3> <table> { for $item in $data/data/operational/project/timesheet/ ss:Workbook/ss:Worksheet/ss:Table/ss:Row[not(position()=1)] return <tr> <td>{$item/ss:Cell[1]}</td> <td>{$item/ss:Cell[2]}</td> <td> { let $hours := xs:decimal(($item/ss:Cell[3] div 40 ) * 100) return if ($hours < 100) then utility:graph("bhs", "4D89F9,C6D9FD", "150x30", "0|40", $hours, $item/ss:Cell[3]) else utility:graph("bhs", "FF5858,C6D9FD", "150x30", "0|40", $hours, $item/ss:Cell[3]) } </td> </tr> } </table> |
XQuery 讓您避免使用第一行,因為該行包含了列標籤。另外一個需要實現的功能是當員工每周的工作時間超過 40 小時時,能讓條形圖顯示為紅色。 XQuery 內的 if|then|else 語句的作用是根據 $hours 變數的值是否大於 40 來選擇一個條形。
Microsoft 也開始提供基於開放標準的 XML 源代碼格式,這打破了很多年來一直使用二進位格式的局面。這些格式確實很巧妙,但並不開放,因此很難實現本文所示的這種集成。代表電子數據表的這個 Excel XML 格式是順理成章的,不過,有關其特定結構的優缺點,我留給別人去討論吧。
站點運行
由 data.xq 生成的測試元素能夠展現此 Web 站點是否在線以及需要多久 Web 伺服器才會響應。為了簡便起見,我只對 status 屬性運行了一個測試,如 清單 12 所示。
<h3>Website Operation <sup>(xquery logic)</sup></h3> <table> { for $item in $data//website_ok/website return <tr> <td>{$item/@title/string()}</td> <td> { if ( $item/test/@status = '200' ) then attribute style{'background-color:green'} else attribute style{'background-color:red' } }  </td> <td>{ $item/test/@response/string() } ms</td> </tr> } </table> |
使用類似的函數不僅能進行存在與否的測試,而且還能更近一步。比如,可以測試聯繫人表單是否工作。
Reminders
有了來自另外一個 Web 應用程序(http://www.backpackit.com/)的數據,我使用 XQuery FLWR(for、let、where 和 return)表達式對一個 XML Web 服務進行迭代,如 清單 13 所示。
<h3>Reminders <sup>(simple webservice)</sup></h3> <table> { for $item in $data/data/operational/project/backpack/reminders/reminder return <tr> <td>{ string($item/creator/@name) }:</td> <td> { $item/content }</td> </tr> } </table> |
Competitor Website
為了啟發讀者,我還包括了一些額外的小部件。在這種意義上,一個指示板更像是由多種技術混合搭建起來的。compete.com Web 站點能很好地對比顯示幾個 Web 站點的性能。
結束語
在本文中,我構建了一個示例指示板,只是為了展示如何使用 XQuery,因此沒有創建一些快捷或高度可用的東西。如下是一些優化建議:
有了這三個直白的 XQuery 文件,您就可以實現一個能彰顯 XQuery 和 XML 在聚集和顯示數據方面的威力的 Web 指示板了。(責任編輯:A6)
[火星人 ] 用 XQuery 製作指示板已經有570次圍觀