(原文地址:https://bost.ocks.org/mike/bar/2,原文作者:Mike Bostock)
简介:出于个人学习目的,计划将 D3 的学习资料逐步翻译为系列文章,鉴于本人水平有限,如有不到或错误之处,欢迎大家指正,谢谢!
在本教程的前一部分我们用 HTML 只做了一个基础的柱状图,在本篇中,我们将使用 SVG 来实现该柱状图,同时通过加载外部的 TSV 格式数据来让应用情景更加真实。
SVG 介绍
HTML 绝大部分局限于方框形状的绘制,而 SVG 则对贝塞尔曲线、渐变、剪切和蒙版提供了强大支持。虽然我们不需要 SVG 的所有扩展特性来实现一个简单的柱状图,但是当我们设计可视化时,学习 SVG 语法是对我们增加视觉编程词汇的必备扩充。
像其他事物一样,丰富的功能往往伴随代价,庞大的 SVG 规范局限了我们的视野,但要记住,我们并不需要掌握所有的特性才能入门。通过浏览这些例子是学习新技术的快乐方法。
虽然 SVG 和 HTML 有明显的差异,但二者也分享许多共通性。您可以将 SVG 标记语言直接嵌入网页代码,也可以通过浏览器的开发者工具检查 SVG 元素。SVG 可以使用 CSS 进行样式化,但二者使用了不同的标签名称(比如 SVG 使用 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 的样式和属性二者差异所迷惑,样式属性的完整列表在 SVG 规范之中被明确定义。我们可以简单的做如下归类:几何形状(如方框的长与宽)必须使用属性定义,而外观内容(如填充颜色)可以使用样式表定义。虽然您可以用属性定义一切,为了确保内联样式能够与样式表的正确交互,我建议您使用样式表表达外观部分。
SVG 要求文字内容被明确放置在 text
元素内,但由于其 text
元素不支持填充和边界,文字的定位必须与柱子顶端保持 3 像素的偏移量,使用 dy
偏移量来让文字垂直居中。
虽然与 HTML 的许多规范都不同,我们看到的图表结果与前一篇的图表保持了一致。
自动编码图表
下面,让我们用 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
元素的尺寸,这样图表的高度就可以可以自动适应,而不是让图表高度决定柱子宽度。
每个柱子由一个包含rect
及 text
的 g
元素组成,我们使用数据接合(enter
选择器)来为每个数据创建图表,然后将 g
元素垂直移动,从而为布置柱子本身及其标签创造一个自身原点。
因为每个 g
元素只有一个 rect
和一个 text
元素,我们可以将其直接追加至 g
而不需要更多的数据接合,仅仅在基于数据创建不同数量的子元素时需要数据接合。被追加的 rect
和 text
继承了它们父元素的数据,所以我们可以直接使用该数据计算柱子宽度和标签位置。
加载数据
下面让我们通过获取一个独立的数据文件并嵌入,来让这个例子更加真实。外部的数据文件将该实现与数据进行分离,让该实现对不同数据集甚至实时变化的数据的重用更加容易。
分栏符分隔的数据集(TSV)是一个常用的表格数据结构,该数据可以通过微软 Excel 表格或其他表格程序、乃至通过文本编辑器手写获得。数据的每一行代表一个表格行,每个行则包含通过分栏符分隔的不同列。第一行表头表达了对应列的名字,与之前全是数据的表格不同,我们将增加一个名字列,内容如下所示:
name value
Locke 4
Reyes 8
Ford 15
Jarrah 16
Shephard 23
Kwon 42
想要在浏览器中使用该数据,我们需要从网络服务器下载该文件并处理它,将该文件的内容转换为可用的 JavaScript 对象。幸运的是,我们可以通过一个简单的函数实现该任务:d3.tsv
。
加载数据增加了新的复杂度:下载内容是异步的。当我们呼叫 d3.tsv
时,该函数会立即返回而不会理会在后台继续下载的内容。只有在未来某时下载完成时,回调函数会提供下载的数据,或汇报下载的失败。通过以下代码我们表达执行情况:
// 1. 这里在下载开始前运行
d3.tsv("data.tsv", function (error, data) {
// 3. 回调函数,这里的代码会在下载成功后最后运行
});
// 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-scale
,我们无法在数据加载前定义其数值范围,因为该范围取决于数据的最大值。因此,我们再回调函数内声明了该范围,同样的,虽然图表宽度可以静态设置,图表的高度仍取决于在回调函数中获取的数据的长度。
现在,我们的数据集包含了名称与数值二者,我们必须用 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
取代,特别是由于我们需要通过缩放决定柱子的宽度,我们必须使用函数来获取数值传入缩放组件: function (d) { return x(d.value); }
。同时,在计算数据集最大值时,我们必须为 d3.max
提供一个函数来告诉它如何评估每个点的数据。
由于使用了外部数据,我们还有一个要处理的地方:数据类型。名字成员包含的是字符串而数据成员包含的是数字,不幸的是,d3.tsv
并不足以自动检测和转换数据类型,我们必须指定一个作为 d3.tsv
的第二个参数的函数来进行转换,该转化函数可以根据每一行修改数据,从而令其变成更适合的表达方式:
function type (d) {
d.value = +d.value; // 强制转换为数字
return d;
}
类型转换并不是严格需要,但它是一个很好的注意。默认情况下,TSV 和 CSV 的所有列都是字符串,如果您忘记将字符串转换为数字,JavaScript 可能不会做到你所期待的结果,比如将 "1" + "2" 返回 "12" 而不是 3。相似的,如果你将字符串而不是数字排序,d3.max
的字符串排序结果可能会吓到你。
下一篇:第三部分
虽然我们的柱状图没有我们之前创建的手写代码那么令人印象深刻,但这里我们介绍了两个非常重要的现实中数据呈现的内容:SVG 和外部数据。在这里,我们更好的完成了之前的图表,但在下一篇教程之中,我们将介绍坐标轴及图表风格。
个人笔记
在本篇教程之中,作者通过用全新的方法来实现与之前相同的柱状图例子让读者登堂入室,进入了 D3 图形编程真正的大门,同时对之前的许多概念进行了复习和强化。
本篇文章除了增加 SVG 和数据获取内容之外,与前一篇之间没有带入很高的学习难度,但这里呈现的图表绘制过程,将在读者未来的所有图表应用之中其到模板式的作用。
从模块组成上来讲,D3 的诸多模块让我们初学者容易摸不清头脑,也不知道该从哪里入手学习绘制图表,但本篇教程是一个入门的很好的例子,虽然内容并不复杂,却描述了所有 D3 图表绘制工程的一般过程。