作者:何宗諭 / 臺灣大學計算機及資訊網路中心程式組幹事
D3.js(Data-Driven Document)照其字面意思就是以data driven的方式在網頁上動態地操作DOM(Document Object Model)。提到DOM的操作,會讓我們聯想到另一套知名JavaScript函式庫jQuery,D3.js和jQuery有很多相似的地方,比起jQuery廣泛性應用,D3.js則是專注在資料視覺化的呈現,其中一個原因在於D3.js原生支援SVG向量式圖形,SVG可以更有效率於網頁上存放圖形。但早期HTML本身並不支援SVG格式(直到HTML5),因此過去若要使用SVG,都要使用額外的套件。而D3.js原生語法支援SVG,可以更有效率地動態處理圖形,所以也有人稱D3.js為jQuery的SVG版。
圖一、Data-Driven Documents (from https://d3js.org/)
而D3.js另一個與jQuery不同的地方在於,D3.js利用data driven的方式可以更有效率的處理data。若要將大量的資料存放在DOM裡,過去我們會用迴圈的方式來批次處理資料,而D3.js則是利用原生的函式,可以更有效率地處理大量的資料。舉例來說,要將一筆陣列資料myData=[ 1, 2, 3, 4, 5, … ]寫到網頁上,我們可以定義好預設的標籤(例如<div>),然後使用document.getElementsByTagName("div")的指令,再用(for和each)來處理資料。而在D3.js不需要事先定義好的標籤,只需要selectAll("div")和data(mydata),即會動態地增加或編輯此標籤內容。如此一來讓程式有更多的彈性,並且能動態地即時地作出反應,這是D3.js的其中一個優點。如圖二所示,D3實作迴圈的寫法,使得我們只要使用selectAll的方法即可達到設定標籤的內容。
圖二、比較不同的Selector方法(from https://d3js.org/)
二、實作D3.js的第一個例子
D3.js為JavaScript的擴充函式庫,且目前瀏覽器都相容JavaScript的語法,因此只要載入D3.js的函式庫,就可以在網頁上操作D3.js。首先,我們先去D3.js官網,下載最新的函式庫,但為了版本更新的問題,一般建議使用超連結載入函式庫,其方法如下。
<script src=https://d3js.org/d3.v3.min.js charset="utf-8"></script>
接下來為了實際用console端操作範例,先建一個空的html檔來練習。其內容如下,其中加入了D3.js的連結。
<!DOCTYPE html>
<html>
<head>
<script src=https://d3js.org/d3.v3.min.js charset="utf-8"></script>
<title>D3.js Demo </title>
</head>
<body>
</body>
</html>
用記事本開啟空的文件,填入上述內容,並存成demo.html,接著用瀏覽器開啟(本示範用Chrome操作)這個空的demo.html。因為沒有加入程式碼,此時瀏覽器顯示為空。開啟後用F12,進入console模式,如果已經成功載入D3.js,則在console輸入d3. ,就能利用IntelliSense自動帶出函式,其結果如圖三所示。
圖三、使用Chrome 的Console確認D3.js已正確載入。
確定瀏覽器正確載入D3.js函式後,我們現在來動態新增元件。舉例來說,我們要在網頁上新增5個<div>元件,每一個元件以編號來表示。
其程式碼如下:
var myData = [1,2,3,4,5];
d3.select('body')
.selectAll('div')
.data(myData)
.enter()
.append('div')
.text(function(d){return d;});
簡單介紹一下上述的程式碼:
�� myData是我們自行定義的陣列,希望把它顯示在網頁上。
�� selectAll是選取網頁上所有的
,以我們現在的例子來說,網頁上並沒有<div>元素,它就等同於一個空的選擇,因此利用enter(),就會新增標籤。selectAll可以想像是D3上用於簡化取代for迴圈的一個函式。
data(myData)是用來綁定已設定的資料。
enter()是用來比對目前網頁中所有<div>的元素,若<div>的元素量比資料少時,就會新增(這其中原理下一節會著重說明)。
append('div')則是將myData設定於<div>上,依本例就是新增5個新的<div>於網頁上。
而最後的匿名函式function(d),則是將資料顯示於網頁的text上。
圖四、動態新增<div>元素。
圖五、檢示網頁中自動生成的<div>元素
從圖五的例子,我們知道透過enter()函式新增了5個<div>標籤於網頁上,enter()會去比對網頁中的<div>元素個數,假若目前myData資料量大於元素數量,則會新增元素。但假若元素數量比較多,則不會新增,所以這邊我們面臨了一個D3核心的問題,假若網頁上現有的元素個數,與我們存入的資料量不相同時,我們要如何處理?下一節會針對這個問題繼續深入探討。
三、D3.js的動態操作核心概念
D3.js有三個核心的功能:enter、update、exit,這三個主要功能就是用來動態控制網頁上的元素,可以想像對應成一般的新增、修改、刪除等三個步驟,那D3是如何達成的,以下簡單介紹其重點:
Enter:比對網頁上的元素量,若資料數量大於元素數量,則新增元素。
Update:比對網頁上的元素量,若資料等於元素數量,則更新元素。
Exit:比對網頁上的元素量,若資料數量小於元素數量,則刪除元素。
為了更清楚了解操作的方式,以下用一連串的例子來說明,首先我們開啟一個空的html,在console下用enter()新增一筆Data,如圖六所示。
圖六、用Enter()動態新增1筆資料 (data > 空element) 。
接下來繼續使用enter(),新增[2,3,4]三筆資料,如圖七所示,這邊有趣的地方,在於並沒有將元素更新為[2,3,4],反而是變成了[1,3,4],那是因為enter()僅僅去比對網頁上的元素,發現目前資料比元素多了2筆,因此只新增[3,4]的資料,若要改變其元素,則必須使用update的方法。
圖七、用Enter()新增2筆資料 (data > element)。
接下來使用update的方式更新網頁,將新的Data設為[2,4,6],因為資料量與元素量一樣,所以三個元素會從[1,3,4]被更新成[2,4,6],如圖八所示,假若只嘗試更新兩筆資料,例如[10,20],因為update是要對應到一樣的元素個素,所以只會更新前兩項,如圖九所示。
圖八、用Update更新3筆資料 。(data = elements)
圖九、用Update更新2筆資料。(data < elements)
接下來最後一步,則是要利用exit()和remove()來刪除資料,假若我們設定新data為[1],則會刪除目前[20,6]的元素。如圖十所示,最後將資料更新為[1]。
圖十、用Exit刪除2筆資料。(data < elements)
簡單結論來說,enter()和exit(),可以想像成是一種過濾器,enter()是過濾出多出來的資料(data > elements),然後用append()新增資料,而exit()則是去過濾缺少的資料(data < elements),然後利用remove()來刪除元素。
以上利用簡單的範例來說明如何使用D3函式來操作元素,有了這幾個功能,就不用花心思去設計修改標籤,用很簡易的方法即可動態控制DOM,再搭配SVG圖像就可以作出各式各樣的動態圖示操作。以下提供一個簡單的程式碼,將其另存成html檔即可用瀏覽器(測試如圖十),圖十其原始碼出自於http://kuro.tw的作者。
圖十一、利用Enter()和Exit()動態變更長條圖數量。
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<title>D3.js Demo </title>
<style>
.wrap{
position: relative;
overflow: hidden;
margin-bottom: 1em;
}
.bar{
background-color: navy;
width: 2em;
height: auto;
margin-right: 5px;
float: left;
position: relative;
color: #fff;
text-align: center;
padding-top: 5px;
}
button{
font-size: 1.5em; float: left;
margin-right: 10px;
}
</style>
</head>
<body>
<script>
var data = [1, 2, 3, 4, 5];
var height = 250, width = 300;
// body 與 容器
var body = d3.select('body');
var wrap = body.append('div')
.style({
'height': height + 'px'
})
.classed('wrap', true);
// render, & update
var render = function () {
wrap.selectAll('.bar')
.data(data)
.enter()
.append('div')
.classed('bar', true)
.text(function (d) {
return d;
})
.style({
'height': function (d) {
return d * 25 + 'px';
},
'top': function (d) {
return (height - d * 25) + 'px';
}
});
};
// remove
var remove = function () {
wrap.selectAll('.bar')
.data(data)
.text(function (d) {
return d;
})
.style({
'height': function (d) {
return d * 25 + 'px';
},
'top': function (d) {
return (height - d * 25) + 'px';
}
})
.exit()
.remove();
};
// 繪製原始資料
render();
// 兩顆按鈕
body.append('button')
.classed('add', true)
.text('add');
body.append('button')
.classed('remove', true)
.text('remove');
d3.select('.add').on('click', function () {
data.push(Math.floor(Math.random() * 10 + 1));
render();
});
d3.select('.remove').on('click', function () {
data.pop();
remove();
});
</script>
</body>
</html>
四、SVG圖像處理
上一節主要是介紹D3的處理資料概念,前面有提過,D3.js最大的優勢在於資料視覺化的呈現,所以本節將花一點篇幅教學如何將資料轉變成圖像化。
HTML5的SVG圖像顯示
HTML 5本身內建SVG的顯示,因此只要在網頁上加入SVG的標籤後,即可以在網頁上畫出SVG圖像。舉例來說,想要畫出圓和矩型的圖型,即使用circle和rect的標籤即可以畫出如圖十一所示,width和height是畫框的邊界長與寬,而cx和cy則是在此畫框上的相對(x, y)座標(起始原點位於左上角,cx="25"向右25單位, cy="25"向下25單位),因此如圖十一畫出了兩個相距50單位的圓形,由於HTML的SVG內容很多,其它更詳盡的內容可以參考W3School的範例介紹。
<svg width="100" height="100">
<circle cx="25" cy="25" r="25" fill="Red" />
<circle cx="75" cy="25" r="25" fill="Blue" />
</svg>
<svg width="100" height="100">
<rect cx="0" width="100" height="100" fill="Green" />
</svg>
圖十二、HTML原生SVG標籤。
D3.js的SVG圖像顯示
D3.js的目的也是產生SVG標籤,但希望能透過data driven的方式來與資料連結。在上面的HTML5的範例來看,如果我們需要畫兩個以上的圓形時,就要在svg標籤內建立多個圖形的標籤(例如circle),可是這樣一來就不容易與資料動態結合,因此D3.js利用其動態擴充性,即可以將資料與圖形榜定。以下是一個簡單的範例,利用D3.js函式我們希望建立四個不同x軸座標的圓,假設已知輸入的x座標為data = [25, 75, 125, 175],首先在空白已載入D3.js中開啟Console,並例用以下的程式碼,動態產生圓形,如圖十二所示。
// 在body下建立一個空白的svg標籤。
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 100);
// 將圓的屬性與Data結合。
var circleData = svg.selectAll("circle")
.data([25, 75, 125, 175]);
//利用enter(),產生圓。
var circles = circleData.enter().append("circle")
.attr("cy", 25)
.attr("cx", function(d) { return d; })
.attr("r", 25)
.style("fill", function(d) {
if (d <= 25) return "Red";
else if (d<=75) return "Green";
else if (d<=125) return "Blue";
else return "Yellow"});
圖十三、D3產生SVG圖形範例。
利用D3.js 畫出圓餅圖
D3.js可以畫出圓餅圖,其內容網路上很多可以參考,有興可以參考Dashing D3.js或bl.ock.org,利用讀取csv檔,即可以畫出圓餅圖,但其程式碼稍微複雜點。所以本文推薦另一個使用D3.js架構開發的函式Plotly。本函式需先加入plotly的函式庫,如下html預設文件。
<!DOCTYPE html>
<html>
<head>
<script src=https://d3js.org/d3.v3.min.js charset="utf-8"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<title>D3.js Demo </title>
</head>
<body>
</body>
</html>
接著利用Console練習加入如下的程式碼。
// 在body下建立一個空白的div標籤。
d3.select("body").append("div").attr("id", "mydiv");
//設定欲顯示的資料。
var data = [{
values: [80, 40, 20],
labels: ['臺北', '臺中', '高雄'],
type: 'pie'
}];
//設定框架大小。
var frame = {
height: 500,
width: 500};
//使用Plotly畫出圓餅圖。
Plotly.newPlot('mydiv', data, frame);
圖十四、圓餅圖範例。
五、結論
本文花比較多的篇幅在介紹D3.js的data driven概念,但對於D3.js的應用,網路上資源非常多,所以有興趣的讀者可以自行參考官網提供的範例:https://github.com/d3/d3/wiki/Gallery。其中這些範例的程式碼都不用在修改,只要設定JSON檔的資料內容,即可以將資料圖像視覺化。
六、參考資料
1. D3.js官網:https://d3js.org/
2. Kuro.tw部落格:http://kuro.tw/categories/D3-js/
3. W3School HTML5下的SVG應:http://www.w3schools.com/html/html5_svg.asp
4. Ployly官網:https://plot.ly/