最近我一直在做數據可視化的前端工作,我用的最多的繪圖工具是d3。d3有點像photoshop,功能很強大,例子也很多,但是學習成本也不低,做項目是需要較大人力投入的。3月底由在亞馬遜工作的同學介紹下使用了一下echart,一個由百度前端發起的canvas國產類庫(官網:http://echarts.baidu.com/index.html)。這個echart其實是在canvas類庫zrender的基礎上做的主題圖庫,優點有數據驅動,圖例豐富,功能強大,支持數據拖拽重計算,數據區域漫游,全中文文檔非常過癮。跟同樣是國產的前端腳手架fis一樣(官網http://fis.baidu.com/),都是誠意滿滿的國產套餐,體現了現今國內不俗的前端開發實力。使用它們的感覺就像想自己做個平板電腦,去華強北一轉,主板、CPU、屏幕等各種套餐一訂購,東西就嘩嘩地組裝起來了。極其高效,非常適合商業項目開發。而且,即使是為了研究,用這些也可以打一個很好的基礎。

Echart圖表庫層次關系
1.引入目錄
廢話不多說了,看到好東西,第一步是從github上把相關文件全下下來,然后到build目錄翻箱倒櫃把東西找齊。新建目錄如下:
echarts-1.3.8
—-zrender //zrender是echart依賴的繪圖庫,官網要求下載,但是目前我的程序中並沒有直接引用它,可以說普通情況下echart可以自己獨立運行
——–zrender.js
—-excanvas_r3 //excanvas是實現IE7,8兼容canvas繪圖的利器,實現了大部分canvas的API,在繪圖方面其核心是通過IE的VML去實現的,效率較低
——–excanvas.js
—-echarts.js //echarts主程序,包含除map以外所有的主題圖庫。注意這個是壓縮過的,並且只能通過requirejs或者esl.js模塊化加載;想用標簽或sea.js加載請用echarts-plain.js
—-echarts-orginal.js //沒有壓縮過的echarts.js
—-echarts-map.js //echarts的map主題圖庫
2.在自己的項目中實現其demo
這個echart是百度前端做的,他們推薦使用模塊式開發。好在我之前的項目,就是采用requirejs + angularjs開發的。所以引入比較容易。
首先,在requirejs的入口配置文件main.js里加上echart:
require.config({ baseUrl:'application/views/frontEnd/build/' ,paths:{ //這里省略若干配置信息... //echart及其組件 ,echarts: 'lib/echarts-1.3.8/echarts' ,"echarts/chart/line": 'lib/echarts-1.3.8/echarts' ,"echarts/chart/bar": 'lib/echarts-1.3.8/echarts' ,'echarts/chart/scatter': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/k': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/pie': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/radar': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/map': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/chord': 'lib/echarts-1.3.8/echarts' ,'echarts/chart/force': 'lib/echarts-1.3.8/echarts' ,zrender: 'lib/echarts-1.3.8/zrender/zrender' } ,priority:[ 'angular' ] //,urlArgs:'v=1.1' });
然后,在要引入echart的具體頁面控制js文件里,加載相關依賴。
define([ 'echarts', 'echarts/chart/pie', 'd3' ], function (ec) { function common_chart_staff_assess_ctrl($http, $scope) { // angularjs的控制器.... } } return common_chart_staff_assess_ctrl; });
以上兩步,熟悉dojo或者requirejs的朋友應該都毫無壓力,但是還是有很多朋友沒用過這些,所以還是有必要說一下的。
接着就是把官網的例子放在自己的頁面里實現。這里我選取了一個特別的千層餅圖,點擊這里去官網查看原圖。
那么我們把例子里的靜態數據(option對象)搬運到我們自己的JS文件中,然后仿照官網的例子寫一個渲染&刷新函數
//渲染&刷新函數 $scope.refresh = function(option,isBtnRefresh){ if (isBtnRefresh) { needRefresh = true; if (needRefresh) { myChart.showLoading(); setTimeout($scope.refresh(option), 500); } return; } needRefresh = false; if (myChart && myChart.dispose) { myChart.dispose(); } myChart = ec.init(domMain); window.onresize = myChart.resize; myChart.setOption(option, true); domMessage.innerHTML = ''; }; //測試數據 $scope.option = { title : { text: '瀏覽器占比變化', subtext: '純屬虛構', x:'right', y:'bottom' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }, legend: { orient : 'vertical', x : 'left', data:['Chrome','Firefox','Safari','IE9+','IE8-'] }, toolbox: { show : true, feature : { mark : {show: true}, dataView : {show: true, readOnly: false}, restore : {show: true}, saveAsImage : {show: true} } }, calculable : false, series : (function(){ var series = []; for (var i = 0; i < 30; i++) { series.push({ name:'瀏覽器(數據純屬虛構)', type:'pie', itemStyle : {normal : { label : {show : i > 28}, labelLine : {show : i > 28, length:20} }}, radius : [i * 4 + 40, i * 4 + 43], data:[ {value: i * 128 + 80, name:'Chrome'}, {value: i * 64 + 160, name:'Firefox'}, {value: i * 32 + 320, name:'Safari'}, {value: i * 16 + 640, name:'IE9+'}, {value: i * 8 + 1280, name:'IE8-'} ] }) } series[0].markPoint = { symbol:'emptyCircle', symbolSize:series[0].radius[0], effect:{show:true,scaleSize:12,color:'rgba(250,225,50,0.8)',shadowBlur:10,period:30}, data:[{x:'50%',y:'50%'}] }; return series; })() }; setTimeout(function(){ var _ZR = myChart.getZrender(); // 補充千層餅 _ZR.addShape({ shape : 'text', style : { x : _ZR.getWidth() / 2, y : _ZR.getHeight() / 2, color: '#666', text : '惡夢的過去', textAlign : 'center' } }); _ZR.addShape({ shape : 'text', style : { x : _ZR.getWidth() / 2 + 200, y : _ZR.getHeight() / 2, brushType:'fill', color: 'orange', text : '美好的未來', textAlign : 'left', textFont:'normal 20px 微軟雅黑' } }); _ZR.refresh(); }, 2000);
然后在頁面上找個div,顯示echart就可以了。
不過請注意一定要給這個div設置寬度高度,否則圖顯示不出來不要怪我。
<section class="span12"> <div id="graph" class="graph chart-area" style="height:500px"></div> </section>
3.定制
當然,僅僅停留在引用別人的例子是不能讓我滿意的。
首先,要進行數據綁定。
這里細分為3步:
- 分析數據格式(就是分析option對象,這一步需要注意把后台獲取的數據和樣式信息分開。故而我在代碼里設置了$scope.option對象存儲默認的樣式配置信息,之后ajax獲取數據后將二者再extend即可。)
- 在后台組裝相應的數據(根據不同需求而異,我這里只取數值信息,不多言)
- 前台ajax獲取真實數據(這一點相信大家都很純熟,不過我的代碼里是使用angularjs封裝過的$.post對象來做到這一點的)
-
//設置真實數據格式 $scope.default_option = { title : { text: '故障類型時序變化年輪圖', subtext: '本圖由內向外展示了各種故障類型的出現頻率所占百分比,及其隨時間變化的規律', x:'right', y:'bottom' }, tooltip : { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }, legend: { orient : 'vertical', x : 'left', selected: $scope.faultnameSelected ,data:$scope.faultname }, toolbox: { show : true, feature : { mark : true, dataView : {readOnly: false}, restore : true, saveAsImage : true } }, calculable : false, series : {} }; $scope.ajaxChartData = function(dataname){ myChart.showLoading({ text: '正在努力的讀取數據中...', //loading話術 }); $http.post("index.php/main/readfaultnum", null).success(function(alldata){ console.log(alldata); var option = $scope.default_option; option.series = $scope.setData(alldata); $scope.refresh(option,true); $scope.order = dataname; myChart.hideLoading(); }).error(function(){ domMessage.innerHTML = '網絡故障,獲取數據失敗'; }); };

5.進一步定制
雖然我使用了官網的例子實現了需求,但是我還有些自己的想法。
官網的千層餅圖中,代表時間的年輪寬度是固定的。但是我們從后台讀取的數據(以月為顆粒度)在時間上是變化的,隨着系統使用時間增長而增長。這就導致一個問題,就是在只有一個月時,年輪只有小小一個,有50個月時,年輪多到超過了顯示范圍。這樣很不美觀,並且喪失了一定的可用性。
我決定要對其進行優化。當使用月份很少導致數據很少的時候,年輪會很寬;當使用月份很多導致數據很多的時候,年輪會很細——這樣就可以同時避免數據量小的時候不美觀和數據量大的時候喪失可用性的問題了。當然,如果數據量過大,年輪就會過細,同樣會喪失可用性。因此我們要設置一個最大數據量的閾值,把超過的部分砍掉。
解決方案就是要對根據數據量對年輪寬度進行縮放,也就是使用比例尺函數。在echart里我暫時沒有找到這樣的比例尺函數。當然,線性比例尺很簡單,函數可以自己寫。但是其他類型的比例尺縮放就稍微要一些技巧了。好在之前我一直是使用d3類庫來做可視化的,我知道d3里有這樣的比例尺函數可以很容易地解決我的問題。那就是d3.scale對象,其中包含線性比例尺、平方比例尺、指數比例尺、集合比例尺,完全可以滿足需要。我可以從開源的d3庫中把比例尺函數抽取出來,也引入到這個頁面。根據實際數據的測試結果,我選擇了平方比例尺。那么接下來就很好寫了:
var maxTime = 36; //本千層餅圖最多顯示60個月的數據(最多顯示60個圈) //原始數據的加工工廠函數 $scope.setData = function(data){ //這是D3的平方比例尺函數,用於根據數據大小縮放環的寬度 var rScale = d3.scale.sqrt() .domain([maxTime, 1]) .range([3, 30]) .nice(); console.log(rScale(1), rScale(12), rScale(36)) //如果數據量超過60(即有60個月),則刪除60個月之前的數據,只顯示最近60個月的內容 if(data.length > maxTime){ data.splice(0, maxTime); } //經過D3比例尺計算的環的寬度 var R = rScale(data.length); console.log(R); for(var i=0; i<data.length; i++){ data[i]['type'] = 'pie'; data[i]['radius'] = [i * R + 40, i * R + 40 + R]; //if(typeof($scope.times[i])!= null) data[i]['name'] = $scope.times[i]; data[i]['itemStyle'] = {normal : { label : {show : i > (data.length-2)}, labelLine : { show : i > (data.length-2), length:40, color : '#f0f', width : 10, type : 'dotted' } }}; } //顯示中央文字 setTimeout(function(){ var myDate = new Date(); var myMouth = myDate.getMonth()+1; var myYear = myDate.getFullYear(); //獲取完整的年份(4位,1970-????) // 補充千層餅中央的說明文字 var _ZR = myChart.getZrender(); _ZR.addShape({ shape : 'text', style : { x : _ZR.getWidth() / 2, y : _ZR.getHeight() / 2, color: 'orange', text : data.length + '個月前', textAlign : 'center' } }); _ZR.addShape({ shape : 'text', style : { x : _ZR.getWidth() / 2 + data.length * R +50, y : _ZR.getHeight() / 2, color: 'orange', strokeColor: 'pink', text : '今日 ' + myYear + "年" + myMouth + "月" , textAlign : 'left', textFont:'normal 14px 微軟雅黑' } }); _ZR.refresh(); }, 500); return data; };

只有4個月數據時的年輪圖

30個月的測試數據的年輪圖
當然,做到這一步,雖然實現了需求,但畢竟是照着別人的例子修改,沒有自己創作來的有深度。不過如何自己創作,就超過了本文的范疇,並且也不是這一篇博客所能包含得了的,日后再議。
5.瀏覽器兼容性:
最后必須一提瀏覽器兼容性問題。IE8、IE7瀏覽器不兼容canvas繪圖(IE6請允許我直接無視),為了實現兼容需要引入excanvas_r3庫。
1
2
3
4
5
|
<
!
--
Le
HTML5
shim
,
for
IE6
-
8
support
of
HTML5
elements
--
>
<
!
--
[
if
lt
IE
9
]
>
<script
src
=
"application/views/frontEnd/build/lib/html5shiv.js"
>
</script>
<script
src
=
"application/views/frontEnd/build/lib/echarts-1.3.8/excanvas/excanvas.js"
>
</script>
<
!
[
endif
]
--
>
|
有時IE8中繪圖錯位,加入以下代碼啟用IE7兼容模式即可解決:
1
2
3
4
|
<
!
--解決
IE8中
canvas繪圖錯位
--
>
<
!
--
[
if
lt
IE
9
]
>
<
meta
http
-
equiv
=
"X-UA-Compatible"
content
=
"IE=7"
/
>
<
!
[
endif
]
--
>
|