基于 D3.js ,,編寫 HTML,、SVG 和 CSS 就能讓你的數(shù)據(jù)變得生動起來,這是一個基于數(shù)據(jù)操作 DOM 的 JavaScript 庫,。
我看來,,每一位 Web 開發(fā)者最應(yīng)該學(xué)習(xí)的三個 JavaScript 庫就是 jQuery,、Underscore 和 D3。在學(xué)習(xí)它們的過程中,,你將會從新的角度去思考如何寫代碼:jQuery 讓你知道如何用盡量少的代碼,,盡可能多地操作 DOM;Underscore (或者稱之為 lodash)用函數(shù)式的工具改變你寫程序的方式,;而 D3 給了你大量的操作數(shù)據(jù)的工具,,以及圖形化編程的思想。如果你還不了解 D3,,請花一些時間看看它的例子,,體會一下 D3 能做到什么。
這可不是你老爸的圖表庫,。
D3 有極高的靈活性,,它是一個比較基礎(chǔ)的可視化 js 庫,api 和 jQuery 很像,,可以把數(shù)據(jù)和 HTML 結(jié)構(gòu)或者 SVG 文檔對應(yīng)起來,。D3 有豐富的數(shù)學(xué)函數(shù)來處理數(shù)據(jù)轉(zhuǎn)換和物理計算,它擅長于操作 SVG 中的路徑 (path) 和幾何圖形 (circle,、ellipse,、rect…)。
這篇文章旨在給讀者一個 D3 的概覽,,在接下來的例子里,,你會看到輸入的數(shù)據(jù)、數(shù)據(jù)變換和最后的輸出文檔,。我將不會解釋逐個函數(shù)做了什么,,我會把代碼展示給你看,希望從中你能知道大致這些代碼是怎么工作的,。只有在 Scales 和 Selections 的部分我才會重點解釋,。
柱狀圖
codepen中查看代碼
我說過在 D3 中,你能使用到的圖表遠比 Playfair 先生發(fā)明過的要多,,但是學(xué)跑前先走好路,,我們從最簡單的柱狀圖開始,,了解一下 D3 是怎么將數(shù)據(jù)和文檔流結(jié)合在一起的:
d3.select('#chart')
.selectAll("div")
.data([4, 8, 15, 16, 23, 42])
.enter()
.append("div")
.style("height", (d)=> d + "px")
selectAll 方法返回了一個 D3 selection。D3 selection 是一個數(shù)組,,當針對一個數(shù)據(jù)點創(chuàng)建一個 div ,,然后通過 enter() 和 append() 調(diào)用這個 div 時,selection 中的元素就會被創(chuàng)建出來,。
上述代碼中的輸入數(shù)據(jù)是一組數(shù)組:[4, 8, 15, 16, 23, 42],,對應(yīng)的輸出 HTML 結(jié)構(gòu)是:
<div id="chart">
<div style="height: 4px;"></div>
<div style="height: 8px;"></div>
<div style="height: 15px;"></div>
<div style="height: 16px;"></div>
<div style="height: 23px;"></div>
<div style="height: 42px;"></div>
</div>
所有不需要通過 JS 控制的視覺層內(nèi)容都寫到 CSS 中:
#chart div { display: inline-block; background: #4285F4; width: 20px; margin-right: 3px;}
GitHub 貢獻表
只需要更改柱狀圖代碼中的幾行,,我們就能得到一個 GitHub 貢獻表,。
codepen中查看代碼
和柱狀圖不同地方在于,圖表中根據(jù)數(shù)據(jù)變化的不再是元素的高度,,而是元素的 background-color (背景色),。
const colorMap = d3.interpolateRgb(
d3.rgb('#d6e685'),
d3.rgb('#1e6823')
)
d3.select('#chart')
.selectAll("div")
.data([.2, .4, 0, 0, .13, .92])
.enter()
.append("div")
.style("background-color", (d)=> {return d == 0 ? '#eee' : colorMap(d)
})
colorMap 函數(shù)接收的輸入值要在0到1之間,返回的是一個顏色值,,這個值是在以輸入值中兩兩數(shù)據(jù)為顏色值之間的漸變色值,。插值法是圖形編程和動畫的關(guān)鍵點。在后面我們將會看到更多這方面的例子,。
SVG
D3 最大的魅力大概來自于它能應(yīng)用在 SVG 上,,也就是說平面圖形比如圓形、多邊形,、路徑和文本,,它都可以交互。
<svg width="200" height="200">
<circle fill="#3E5693" cx="50" cy="120" r="20" />
<text x="100" y="100">Hello SVG!</text>
<path d="M100,10L150,70L50,70Z" fill="#BEDBC3" stroke="#539E91" stroke-width="3"></svg>
上述代碼實現(xiàn)的是:
一個圓心在 (50,120),,半徑是 20 的圓形;
一段位于 (100,100) 的文本;
一個 3px 粗邊的三角形,,d 指的是方向,從點 (100,100) 畫線到點 (150,170) 再到點 (50,70) 結(jié)束
可以算是 SVG 中很好用的元素了,。
圓形
codepen中查看代碼
上面的例子中給出的數(shù)據(jù)結(jié)構(gòu)是很簡單的一組數(shù)據(jù),,D3 的能力遠不止于此,它還可以操作更復(fù)雜的數(shù)據(jù)類型,。
const data = [{ label: "7am", sales: 20
},{ label: "8am", sales: 12
}, { label: "9am", sales: 8
}, { label: "10am", sales: 27
}]
對每一個數(shù)據(jù)點,,我們都將有一個 g (組)元素在 #chart 中,根據(jù)對象的屬性,,每個組里會有一個 元素和一個 元素,。
下面的代碼將輸入數(shù)據(jù)和 SVG 文檔一一對應(yīng)起來,你能看出它的原理嗎,?
<svg height="100" width="250" id="chart">
<g><circle cy="40" cx="50" r="20"/><text y="90" x="50">7am</text>
</g>
<g><circle cy="40" cx="100" r="12"/><text y="90" x="100">8am</text>
</g>
<g><circle cy="40" cx="150" r="8"/><text y="90" x="150">9am</text>
</g>
<g><circle cy="40" cx="200" r="27"/><text y="90" x="200">10am</text>
</g></svg>
折線圖
codepen 中查看代碼
用 SVG 實現(xiàn)折線圖再簡單不過,,我們將下面這些數(shù)據(jù):
const data = [
{ x: 0, y: 30 },
{ x: 50, y: 20 },
{ x: 100, y: 40 },
{ x: 150, y: 80 },
{ x: 200, y: 95 }
]
轉(zhuǎn)換成以下的 SVG 文檔:
<svg id="chart" height="100" width="200">
<path stroke-width="2" d="M0,70L50,80L100,60L150,20L200,5"></svg>
**注意:**SVG 代碼中的 y 值和輸入值的 y 值不同,是用 100 減去給定的 y 值,,因為在 SVG 中屏幕的左上角是 (0,0),所以在縱坐標最大值是 100 的坐標系中,,需要這么處理。
可以這么實現(xiàn)只由一條 path 構(gòu)成的圖形:
const path = "M" + data.map((d)=> { return d.x + ',' + (100 - d.y);
}).join('L');const line = `<path stroke-width="2" d="${ path }"/>`;document.querySelector('#chart').innerHTML = line;
上面的代碼看著可麻煩了,,D3 其實提供了路徑生成函數(shù)來簡化這個步驟:
const line = d3.svg.line()
.x((d)=> d.x)
.y((d)=> 100 - d.y)
.interpolate("linear")d3.select('#chart')
.append("path")
.attr('stroke-width', 2)
.attr('d', line(data))
清爽多了,!interpolate 函數(shù)可接受不同的參數(shù),畫出不一樣的圖形,,除了 'linear’,,還可以試試看 'basis’、’cardinal’……
Scales
Scales 函數(shù)可以將一個輸入集映射到一個輸出集中,。
codepen中查看代碼
上述例子所用的數(shù)據(jù)都是假數(shù)據(jù),,不會超過坐標軸所設(shè)定的范圍。當數(shù)據(jù)是動態(tài)變化的時候,,事情可就沒有這么簡單了,,你需要將輸入映射到固定范圍的輸出中,也就是我們的坐標軸,。
假設(shè)我們有一個 500px X 200px 大小的折線圖區(qū)域,,輸入數(shù)據(jù)是:
const data = [
{ x: 0, y: 30 },
{ x: 25, y: 15 },
{ x: 50, y: 20 }
]
如果 y 軸的范圍在 [0,30],x 軸的范圍在 [0,50],,那數(shù)據(jù)就能被漂亮地呈現(xiàn)在屏幕上,。不過現(xiàn)實是,y 軸范圍在0到200,,x 軸范圍在0到500間,。
我們可以用 d3.max 獲取到輸入數(shù)據(jù)中的最大的 x 值和 y 值,然后創(chuàng)建出對應(yīng)的 scales,。
scale 和上面用到的顏色差值函數(shù)類似,,都是將輸入值對應(yīng)到固定的輸出范圍中。
xScale(0) -> 0xScale(10) -> 100xScale(50) -> 500
對于超出輸出值范圍的輸入值,,同樣適用:
xScale(-10) -> -100xScale(60) -> 600
在生成折線圖的代碼中 scales 可以這么使用:
const line = d3.svg.line()
.x((d)=> xScale(d.x))
.y((d)=> yScale(d.y))
.interpolate("linear")
scales 還能讓圖形更優(yōu)雅地顯示出來,,比如加上一點間距:
const padding = 20;const xScale = d3.scale.linear() .domain([0, xMax]) .range([padding, width - padding])
const yScale = d3.scale.linear() .domain([0, yMax]) .range([height - padding, padding])
現(xiàn)在可以在有動態(tài)數(shù)據(jù)集的前提下生成該集的折線圖,這條折線圖保證會在 500px X 200px 的范圍內(nèi),,并且距離該區(qū)域的四邊都有 20px 的間距,。
線性的 scale 最常見,不過還有處理指數(shù)的 pow,、處理非數(shù)值數(shù)據(jù)(比如分類,、命名等)的 ordinal scales,除此之外還有Quantitative Scales,、Ordinal Scales 和 Time Scales,。
比如把我的壽命當做輸入值,映射到 [0,500] 的區(qū)域內(nèi):
const life = d3.time.scale()
.domain([new Date(1986, 1, 18), new Date()])
.range([0, 500])// 0 到 500 之間的哪個點才是我的 18 歲生日呢,?life(new Date(2004, 1, 18))