基於D3.js的資料視覺化前端實現方案
近幾年隨著大資料逐漸火熱,資料視覺化也就顯得格外重要,Ben Fry在他的著作《Visualiziing Data》中將資料視覺化的過程分為七個步驟:
- 獲取
- 分析
- 過濾
- 挖掘
- 表現
- 改善
- 互動
前面4步分別屬於資料採集、資料分析、資料處理和資料探勘領域,資料視覺化的處理範圍主要是後面三步。
(一)技術選型
本文是基於實驗室做的智慧客運公交wifi系統對後臺資料進行視覺化處理,分析業務需求主要是將新增關注人數、取消關注人數和每車最大連線數以折線圖的形式動態展示。
整個資料視覺化方案實施近兩個星期,前期進行視覺化技術選型,由於資料視覺化外掛種類和質量參差不齊,其中包括echarts(百度開源)
1、相容性
使用每個外掛之前必須考慮它的相容性問題,否則專案完成後發現部分瀏覽器不能用就需要重新選型或解決相容性問題,事倍功半,得不償失!
- Highcharts 相容 IE6 及以上的所有主流瀏覽器,完美支援移動端縮放、手勢操作。
- Echarts 相容 IE6 及以上的所有主流瀏覽器,同樣支援移動端的縮放和手勢操作。
- D3 相容IE9 及以上的所有主流瀏覽器,對於移動端的相容性也同上。
目前這三種外掛基本滿足相容性要求。
2、是否開源
- Highcharts 非商業免費,商業需授權,程式碼開源。
- Echarts 完全免費,程式碼開源。
- D3 完全免費,程式碼開源。
由於本專案是後臺管理系統做資料視覺化,不涉及商業用途,所有就是否開源方面選擇哪個框架意義不大。
3.難易程度
- Highcharts 基於SVG,方便自己定製,但圖表型別有限。
- Echarts 基於Canvas,適用於資料量比較大的情況。
- D3.v3 基於SVG,方便自己定製;D3.v4支援Canvas+SVG,如果計算比較密集,也可以選擇用Canvas。除此之外,D3圖表型別非常豐富,幾乎可以滿足所有開發需求,但程式碼相對於以上兩個外掛來說,會稍微難一點。
總的來說,我認為Echarts是國內做的最好的圖表庫,有多樣的圖表型別,色彩豐富,互動友好,易上手;Highcharts作為老牌圖表庫,國外開源專案,技術相對成熟,程式碼有社群人員維護,如果專案中對圖表樣式和展現效果沒有嚴格要求,可以考慮用以上兩種。我第一次接觸D3,是在官網上看到的,酷炫的案例讓我眼前一亮,馬上在網上買了一本《精通D3.js》,學習之後發現做資料視覺化是件很有意思的事情。由於秉承著科研精神和工匠精神,加之之前一段時間接觸過D3.v3,做過一些demo,多樣的圖表設計和豐富的展示效果深深吸引著我,所以專案做對後臺資料視覺化處理我選擇D3.js作為開發工具。
(二)方案設定
完成技術選型之後,接下來就是方案設定,基於智慧客運公交wifi系統後臺資料視覺化輸出要求,我畫了簡單的互動設計稿,如下圖所示:
主要有四類資料:新增關注人數、取消關注人數、付費人數和最大連線數,其中最大連線數還需選擇車次,這四類可以設計4個控制元件,選擇不同控制元件會呈現出相應的折線圖,車次選擇下拉菜單隻有在點選最大連線數時才顯示。
(三)方案實施
首先約定資料格式為以物件元素組成的陣列型別。如:
dataset=[{x:'2017-11-01',y:20},{x:'2017-11-02',y:18},
{x:'2017-11-03',y:22},{x:'2017-11-04',y:26},
{x:'2017-11-05',y:16},{x:'2017-11-06',y:30},
{x:'2017-11-07',y:22},{x:'2017-11-08',y:28},
{x:'2017-11-09',y:30},{x:'2017-11-10',y:36}]
1、需要構建SVG。SVG可以理解為PS中的畫布或者是一張白紙,可以填充任意顏色甚至可以為透明色,程式碼如下:
svg=d3.select(".container")
.append("svg")
.attr("class","drawSVG");
/* SVG樣式 */
.drawSVG{
display: inline-block;
margin-right:10px;
width: 100%;
height:500px;
}
2、定義比例尺。需求中以近十天的日期作為橫座標,因此在做比例尺定義的時候需考慮非線性比例尺,即序列比例尺,定義域和值域一一對應。在定義比例尺之前需計算縱座標的最大值,程式碼如下:
var gdpmax=0;
//遍歷dataset.y,將字串轉換成數字
//d3.max無法計算字串大小,因此需要將dataset.y中所對應的字串轉換成數字
var newDataset=[];
for(var i=0;i<dataset.length;i++){
numberY=Number(dataset[i].y);
newDataset.push(numberY);
}
var currGdp=d3.max(newDataset);
if (currGdp>gdpmax)
gdpmax=currGdp;
用於每次資料獲取的過程都需通過AJAX呼叫後臺資料,返回的結果在數字變字串,在使用d3.max求最大值的時候,字串如何計算,導致計算錯誤,因此需要將字串轉換為數字,儲存在newDataset陣列中,再利用d3.max(newDateset)計算縱座標最大值。
定義比例尺程式碼如下:
xScale=d3.scaleOrdinal()//實現需要的非線性比例尺
.domain(date)//data=['2017-11-01','2017-11-02',...]
.range(space);//space=[0,90,180,...]
yScale=d3.scaleLinear()//橫座標比例尺
.domain([0,gdpmax*1.1])
.range([height-padding.top-padding.bottom,0]);
由於利用d3.scaleOrdinal()序列比例尺,定義域與值域一一對應,為了讓定義域的個數決定值域個數,我設計了一個小技巧。
var sum=0;
for (var i=0;i<dataset.length;i++){
space.push(sum);
date.push(dataset[i].x);
sum=sum+90;
}//data=['2017-11-01','2017-11-02',...];space=[0,90,180,...]
space陣列的個數有data個數決定,並且間距為90,決定橫座標的單位座標為90。
3、繪製座標軸。在把比例尺定義後之後,繪製座標軸的過程就簡單多了,具體程式碼如下:
/*定義x軸*/
var xAxis=d3.axisBottom(xScale)
.tickFormat(d3.format("d"))
.tickSizeOuter(10)
.tickPadding(10);
/*定義y軸*/
var yAxis=d3.axisLeft(yScale)
.tickSizeOuter(10)
.tickPadding(10);
/*新增一個<g>元素用於放X軸*/
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+padding.left+","+(height-padding.bottom)+")")
.call(d3.axisBottom(xScale));
/*新增一個<g>元素用於放Y軸*/
svg.append("g")
.attr("class","axis")
.attr("transform","translate("+padding.left+","+padding.top+")")
.call(yAxis);
}
4、渲染資料。前面的工作只是在畫板上定好繪畫區域,接下來解決資料繫結和呈現問題,資料繫結是D3處理選擇集和資料的方法,是D3之所以被稱之為D3的原因。選擇集上是沒有資料的,資料繫結就是使選擇集“擁有”資料的過程,主要有以下兩種方法:
- selection.datum([value])
選擇集上的每一個元素擁有相同的資料 - selection.data([value])
選擇集中的每一個元素分別繫結陣列value中的每一項,用的比較多。
svg中有兩個形狀元素可以畫直線,一個是<line>
,另一個是<path>
。<line>
中需要新增x1
,y1,x2,y2屬性,決定線段的起點和終點,但是資料繫結以後,一方面不知道資料值是多少,另一方面手動繫結比較繁瑣,所以想利用<path>
和直線生成器,直接生成線段,簡單粗暴。
/*建立一個直線生成器*/
var linePath=d3.line()
.x(function (d) {
return xScale(d.x);
})
.y(function (d) {
return yScale(d.y);
});
svg.selectAll("path")
.data(dataset)//繫結dataset資料
.enter()
.append("path")
.attr("d",linePath(dataset))//呼叫直線生成器
.attr("transform","translate("+padding.left+","+padding.top+")")
.attr("fill","none")
.attr("stroke-width",2)
.attr("stroke",'#F1C40F')
.attr("class","path");
5、設計控制元件。目前為止,資料視覺化模組基本實現,接下來需建立4個控制元件,監聽控制元件點選事件,並實現相應的資料視覺化。html和css相對簡單些,程式碼如下:
<div class="container">
<div class="btnGroup">
<button data-index="0" id="addPeople">新增關注</button>
<button data-index="1" id="removePeople">取消關注</button>
<button data-index="2" id="maxLinkNum">最大連線數</button>
<div class="bosNum">
<label for="bosName">第幾號車:</label>
<select id="bosName">
<option value="1">1號</option>
<option value="2">2號</option>
<option value="3">3號</option>
<option value="4">4號</option>
<option value="5">5號</option>
<option value="6">6號</option>
<option value="7">7號</option>
<option value="8">8號</option>
<option value="9">9號</option>
<option value="10">10號</option>
<option value="11">11號</option>
<option value="12">12號</option>
<option value="13">13號</option>
</select>
</div>
</div>
</div>
html,body{
height: 100%;
}
*{
margin: 0;
padding:0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.container{
width: 80%;
margin: 0 auto;
border: 1px solid red;
}
/* 按鍵組樣式 */
.btnGroup{
margin: 20px 0 0 20px;
}
/* 按鍵正常樣式 */
button{
width: 100px;
height: 30px;
background: #D35400;
border: 0;
border-radius: 5px;
margin-right: 4px;
color: #fff;
transition: all 0.2s;
outline: 0;
}
/* 滑鼠滑過時按鍵樣式 */
button:hover{
cursor: pointer;
background: #E67E22;
border-radius: 5px;
}
/* 按鍵選中時 */
.active{
background: #F1C40F;
border-radius: 5px;
border: 0;
}
/* 車次選擇樣式 */
.bosNum{
display: inline-block;
font: 14px "sans-serif";
}
select{
width: 100px;
height: 30px;
}
/*
*新增人數觸發事件
*/
addPeople.on("click",function () {
$("svg").remove();
bosNum.hide();//隱藏汽車車次選擇
addPeople.addClass("active");
maxLinkNum.removeClass("active");
removePeople.removeClass("active");
key=addPeople.data("index");//獲取當前addPeople按鍵data-index值
//ajax呼叫後臺資料
$.getJSON("newClients",function(data) {
/*
*新建從0到N的陣列,為後續做虛假座標做準備
*/
dataset1=[];
for (var j=0; j<data.length;j++){
dataset1.push(j);
}
drawSVG();//呼叫構建SVG畫布函式
defindScale(data);//呼叫比例尺和座標軸函式
drawDATA(data);//呼叫渲染資料函式
//延遲1s執行獲取座標系中的焦點函式
setTimeout(function () {
drawFocus(data);
},1000);
})
});
6、友好互動。
(1)讓折線過渡載入。靈感來源於下圖,設計原理利用css中animation動畫效果,和path元素中的stroke-dasharray和stroke-dashoffset屬性,stroke-dasharray:1000;表示定義虛線中實線和虛線的長度都為1000,先實線後虛線;stroke-dashoffset:1000(stroke-dashoffset的值要大於實現線段的長度!),決定虛線距離原初始位置向左的偏移為1000,再利用animation動畫,將stroke-dashoffset:1000減小到stroke-dashoffset:0。
/* 折線過渡動效 */
.path {
stroke-dasharray: 4000;
animation: dash 4s ease-out;
}
@keyframes dash {
0%{
stroke-dashoffset: 4000;
}
100%{
stroke-dashoffset: 0;
}
}
效果如下圖所示:
(2)焦點提示。實現當滑鼠移動到svg是觸發滑鼠移動事件,並通過計算得到焦點座標。程式碼如下圖所示:
svg.append("rect")
.attr("class", "overlay")
.attr("x", padding.left)
.attr("y", padding.top)
.attr("width", space[space.length-1])
.attr("height", height - padding.top - padding.bottom)
/*監聽滑鼠移入事件*/
.on("mouseover", function () {
focusCircle.style("display", null);
focusLine.style("display", null);
})
/*監聽滑鼠移出事件*/
.on("mouseout", function () {
focusCircle.style("display", "none");
focusLine.style("display", "none");
})
/*監聽滑鼠滑動事件*/
.on("mousemove", mousemove);
/*焦點元素*/
var focusCircle = svg.append("g")
.attr("class", "focusCircle")
.style("display", "none");
focusCircle.append("circle")
.attr("r", 4.5);
focusCircle.append("text")
.attr("dx", 10)
.attr("dy", -10);
/*對齊線元素*/
var focusLine = svg.append("g")
.attr("class", "focusLine")
.style("display", "none");
var vLine = focusLine.append("line");
var hLine = focusLine.append("line");
/*滑鼠在透明矩形內滑動時呼叫*/
function mousemove() {
var mouseX=d3.mouse(this)[0]-padding.left;
var mouseY=d3.mouse(this)[1]-padding.top;
var x0=xScale1.invert(mouseX);
x0=Math.round(x0);
/*查詢原陣列中x0的值,並返回索引號*/
var bisect=d3.bisector(function (d) {
return d;
}).left;
var index=bisect(dataset1,x0);
var x1=dataset[index].x;
var y1=dataset[index].y;
/*分別用x軸和y軸的比例尺,計算焦點的位置*/
var focusX = xScale(x1) + padding.left;
var focusY = yScale(y1) + padding.top;
switch(key){
case 0:
/*設定焦點的文字資訊*/
focusCircle.select("text").text(x1 + "新增關注:" + y1 + "人")
.attr("style", "opacity:0.8");
break;
case 1:
/*設定焦點的文字資訊*/
focusCircle.select("text").text(x1 + "取消關注:" + y1 + "人")
.attr("style", "opacity:0.8");
break;
default:
/*設定焦點的文字資訊*/
focusCircle.select("text").text(x1 + "最大連線數:" + y1 + "人")
.attr("style", "opacity:0.8");
}
/*通過平移,使焦點移動到指定位置*/
focusCircle.attr("transform", "translate(" + focusX + "," + focusY + ")");
/*設定垂直對齊線的起點和終點*/
vLine.attr("x1", focusX)
.attr("y1", focusY)
.attr("x2", focusX)
.attr("y2", height - padding.bottom)
.attr("style", "stroke:#666;stroke-dasharray:10");
/*設定水平對齊線的起點和終點*/
hLine.attr("x1", focusX)
.attr("y1", focusY)
.attr("x2", padding.left)
.attr("y2", focusY)
.attr("style", "stroke:#666;stroke-dasharray:10");
}
效果圖如下: