教程的上一節介紹了在HTML中創建一個基本的條形圖;在本節中,我們將要使用[SVG](http://www.w3.org/Graphics/SVG/)擴展這個樣例條形圖,并且通過加載外部TSV【[tab-separated values](http://en.wikipedia.org/wiki/Tab-separated_values)】格式文件數據,來使其更加實用化。
#### **SVG介紹**
鑒于HTML極大制約于矩形圖形,SVG提供了有效的繪畫圖元,比如貝塞爾曲線【Bézier curves】,漸變【gradients】,裁剪【clipping】,和遮罩【masks】。雖然對于一個簡單的條形圖,我們不需要使用SVG的所有擴展特性集,但是學習一下SVG,在設計可視化方案時,對你的視覺積淀是一個有價值的補充。
就像任何事情一樣,這種豐富特性必然要付出代價。龐大的[SVG規范](http://www.w3.org/TR/SVG/)可能讓人望而卻步,但是要記住你不需要一開始就掌握所有特性。[瀏覽樣例](https://bl.ocks.org/mbostock)是一個很有趣的途徑來挑選新的技術。
拋開明顯的不同,SVG和HTML有很多的相似性。你可以書寫SVG標記,并將它直接嵌入到一個web頁面中(前提是你你使用<!DOCTYPE html>)。你可以在瀏覽器開發者工具中觀察SVG元素。并且SVG元素可以由CSS定義樣式,即使使用不同的屬性名字,比如用`fill`代替`background-color`。然而,不像HTML,SVG元素相對于容器的左上角來定位;SVG不支持流式布局,或者
文字圍繞。
#### **手動編寫一個圖表**
在我們使用JavaScript構建一個新圖表之前,讓我們先寫一個SVG的靜態版本。
~~~
<!DOCTYPE html>
<style>
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
</style>
<svg class="chart" width="420" height="120">
<g transform="translate(0,0)">
<rect width="40" height="19"></rect>
<text x="37" y="9.5" dy=".35em">4</text>
</g>
<g transform="translate(0,20)">
<rect width="80" height="19"></rect>
<text x="77" y="9.5" dy=".35em">8</text>
</g>
<g transform="translate(0,40)">
<rect width="150" height="19"></rect>
<text x="147" y="9.5" dy=".35em">15</text>
</g>
<g transform="translate(0,60)">
<rect width="160" height="19"></rect>
<text x="157" y="9.5" dy=".35em">16</text>
</g>
<g transform="translate(0,80)">
<rect width="230" height="19"></rect>
<text x="227" y="9.5" dy=".35em">23</text>
</g>
<g transform="translate(0,100)">
<rect width="420" height="19"></rect>
<text x="417" y="9.5" dy=".35em">42</text>
</g>
</svg>
~~~
像之前一樣,一個樣式表向SVG元素應用顏色和其它美學屬性。但是并不像`div`元素那樣使用流式布局來隱含定位,SVG元素必須相對于原點,使用硬編碼來進行絕對定位。
SVG中一個常見的易混淆點是:區分出那些必須被指定為特性的屬性,和那些可以被設置為樣式的屬性。[樣式化屬性](http://www.w3.org/TR/SVG/styling.html)的全部列表在[SVG規范](http://www.w3.org/TR/SVG/)中有寫明,但這里有一個簡單的經驗法則,就是幾何屬性(比如一個長方形【`rect`】元素的寬度【`width`】屬性)必須被指定為特性,而美學的屬性(比如一個填充【`fill`】色)就可以被指定為樣式。雖然你可以使用特性做任何事,但我還是推薦你使用樣式來做美學方面的工作;這可以保證任何行內樣式在CSS的作用下都能表現的很好。
SVG要求在`text`元素中明確放置文本內容。因為`text`元素不支持內補間【padding】或者外補間【margins】,所以文本內容的位置必然是默認的距離條帶末尾3個像素,而`dy`偏移則是用來垂直居中文本內容。

盡管其實現和之前完全不同,但做出來的圖表和之前的完全相同。
#### **自動生成一個圖表**
下一步讓我們使用D3來構建圖表。到目前,有些部分的代碼會比較熟悉:
~~~
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var data = [4, 8, 15, 16, 23, 42];
var width = 420,
barHeight = 20;
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", barHeight * data.length);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", x)
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d; });
</script>
~~~
我們在JavaScript中設置了`svg`元素的尺寸,這樣我們就可以基于數據集的尺寸(`data.length`)來計算總高度。用這種方式,尺寸基于的是每一個條帶的高度,而不是基于整個圖表的高度,并且可以保證為標簽留有足夠的空間。
每個條帶包含了一個`g`元素,這個元素依次包含了一個`rect`和一個`text`。我們使用了一個數據加載【data join】(一個enter選擇器)為每個數據點創建一個`g`元素。然后我們對`g`元素進行了垂直轉換,創建了一個用于定位條帶及其關聯標簽的本地原點。
由于每一個`g`元素里面都恰好有一個`rect`元素和一個`text`元素,我們也可以將這些元素直接append到`g`元素中,而不需要額外的數據加載。數據加載只有在創建一個基于數據而數目可變的子元素時才需要用到;這里我們對每一個父元素只append了一個子元素。append進去的`rect`元素和`text`元素從它們的父節點`g`元素中繼承數據,因此我們可以使用數據來計算條帶的寬度和標簽的位置。
#### **讀取數據【Loading Data】**
讓我們將數據集提取到一個單獨的文件中,使這個圖表更加實用化。一個外部的數據文件分離開了數據和圖表實現,使其在多個數據集中更容易復用,甚至可用于隨時間變化的實時數據。
制表符分隔的值【Tab-separated values(TSV)】是一個很方便的表格數據格式。這種格式可以從Microsoft Excel或者其它電子表格程序中導出,也可以在文本編輯器中手工編寫出來。每一行代表一個表格行,每一行包含了多列由制表符分隔的數據。第一行是表頭行,指定了列的名字。鑒于之前我們的數據集是一個簡單的數值數組,現在我們增加一個描述性名字列。我們的數據文件現在看上去是這樣子:
~~~
name value
Locke 4
Reyes 8
Ford 15
Jarrah 16
Shephard 23
Kwon 42
~~~
為了在web瀏覽器中使用這些數據,我們需要從一個web服務器中下載這個文件然后轉化它,將文件中的文本內容轉換為可用JavaScript對象。幸運的是,這2個工作可以由一個單獨函數完成:[`d3.tsv`](https://github.com/mbostock/d3/wiki/CSV)。
讀取數據引進了一個新的問題:下載是異步的。當你調用`d3.tsv`,它會立刻返回,而文件下載則是在后臺繼續運行。在未來的某一時刻下載完成時,你的回調函數會被調用,其中含有下載的新數據,或者如果下載失敗的話是一個錯誤。實際上你的代碼是不按順序執行的:
~~~
// 1. Code here runs first, before the download starts.
d3.tsv("data.tsv", function(error, data) {
// 3. Code here runs last, after the download finishes.
});
// 2. Code here runs second, while the file is downloading.
~~~
因此我們需要將圖表實現分為2個階段。首先,當頁面被加載后,數據可用之前,我們盡可能的初始化。當頁面被加載時最好設置好圖表的尺寸,以便數據下載后頁面不會重新布局。然后,我們在回調函數中完成圖表的余下部分。
重構代碼:
~~~
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 420,
barHeight = 20;
var x = d3.scale.linear()
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width);
d3.tsv("data.tsv", type, function(error, data) {
x.domain([0, d3.max(data, function(d) { return d.value; })]);
chart.attr("height", barHeight * data.length);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d.value) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
</script>
~~~
那么,哪里做了改變?盡管我們像之前一樣在同一個地方聲明了x軸的比例尺,但我們在數據被讀取之前無法定義domian域(即定義域),因為定義域依賴于數據最大值。因此,定義域被放在了回調函數中設置。相似的,盡管圖表寬度可以設置為靜態的固定值,但是圖表高度依賴于條帶的數量,因此也需要放在回調函數中設置。
既然我們的數據集包含了名字和值,我們必須使用`d.value`來引用值,而不是使用`d`;每一個數據點都是一個對象而不是一個單獨數值。在JavaScript中對等表達大概看上去是這樣:
~~~
var data = [
{name: "Locke", value: 4},
{name: "Reyes", value: 8},
{name: "Ford", value: 15},
{name: "Jarrah", value: 16},
{name: "Shephard", value: 23},
{name: "Kwon", value: 42}
];
~~~
在老的圖表實現中,所有引用`d`的地方現在都要換成`d.value`。特別的,鑒于之前我們可以使用比例尺`x`來計算條帶寬度,現在則必須指定一個函數將數據值傳給比例尺:`function(d) { return x(d.value); }`。相似的,當從數據集中計算最大值時,我們必須傳入一個存取函數給`d3.max`,告訴它如何評估每個數據點的大小。
這里對外部數據還有一個小技巧:types! `name`列包含了字符串,而`value`列包含了數值。不幸的是,`d3.tsv`并沒有智能到可以自動檢測和轉換類型。所以,我們指定了一個`type`函數作為`d3.tsv`的第2個參數。這個類型轉換函數可以修改每一行代表的數據對象,修改它或者將其轉換為更加適合的形式:
~~~
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
~~~
類型轉換不是嚴格要求的,但它是一個非常不錯的點子。默認的,在TSV和CSV文件中所有列的數據都是字符串。如果你忘了將字符串轉換為數值類型,那么JavaScript可能不會如期運行,比如"1" + "2"會返回"12"而不是3。相似的,如果你排序字符串而不是排序數值,那么`d3.max`的詞典順序表現可能會讓你感到驚訝!
#### **下一步: 第3節**
雖然本節我們的條形圖,可能并不比之前一節的條形圖更加出彩(實際上表面來看沒有任何變化),但是這節介紹了SVG和外部數據讀取,這是任何實際可視化工作中2個關鍵的主題。現在我們已經有了更加充分的準備來完成這個圖表。下一節將會涵蓋坐標軸和圖表樣式。