教程的上一节介绍了在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`偏移则是用来垂直居中文本内容。
![](https://box.kancloud.cn/effa2b46c4a35a85832e3c4f9e34cb84_431x130.PNG)
尽管其实现和之前完全不同,但做出来的图表和之前的完全相同。
#### **自动生成一个图表**
下一步让我们使用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个关键的主题。现在我们已经有了更加充分的准备来完成这个图表。下一节将会涵盖坐标轴和图表样式。