二十、D3 折线图

Abstract

在本章中,您将创建一个带有刻度和标签的折线图。D3 不像 jqPlot 那样是一个图表框架。但是,它允许您向文档中添加可缩放矢量图形(SVG)元素,通过操作这些元素,您可以创建任何类型的可视化。这种灵活性使您能够构建任何类型的图表,一砖一瓦地构建。

在本章中,您将创建一个带有刻度和标签的折线图。D3 不像 jqPlot 那样是一个图表框架。但是,它允许您向文档中添加可缩放矢量图形(SVG)元素,通过操作这些元素,您可以创建任何类型的可视化。这种灵活性使您能够构建任何类型的图表,一砖一瓦地构建。

首先,您将了解如何使用上一章介绍的 D3 命令构建折线图的基本元素。特别是,您将分析经常遇到的比例、域和范围的概念。就如何管理值集而言,这些构成了 D3 库的一个典型方面。

一旦您理解了如何管理值域、刻度和区间中的值,您就可以开始实现图表组件了,比如轴、轴标签、标题和网格。这些组件构成了绘制折线图的基础。与 jqPlot 不同,这些组件并不容易获得,而是必须逐步开发。这将导致额外的工作,但也将使您能够创建特殊的功能。你的 D3 图表将能够响应特殊的需求,或者至少,他们将有一个完全原始的外观。举例来说,您将看到如何向轴添加箭头。

D3 库的另一个特点是使用读取文件中包含的数据的函数。您将看到这些函数是如何工作的,以及如何利用它们来满足您的需求。

一旦掌握了实现折线图的基本知识,您将看到如何实现多系列折线图。您还将了解如何实现图例以及如何将其与图表相关联。

最后,作为总结,您将分析折线图的一种特殊情况:差异折线图。这将有助于您理解剪辑区域路径——它们是什么以及它们的用途是什么。

用 D3 开发折线图

您将开始使用 D3 库最终实现您的图表。在本节和接下来的几节中,您将发现一种不同于 jqPlot 和 Highcharts 等库所采用的图表实现方法。这里的实现是在一个较低的层次,代码更长更复杂;然而,没有什么是你够不着的。

现在,一步一步,或者更好,一砖一瓦,你会发现如何产生一个折线图和组成它的元素。

从第一块砖开始

开始的第一块“砖”是在您的 web 页面中包含 D3 库(有关更多信息,请参见附录 A):

<script src="../src/d3.v3.min.js"></script>

或者,如果您喜欢使用内容交付网络(CDN)服务:

<script src="http://d3js.org/d3.v3.min.jsT2】

下一个“砖块”由清单 20-1 中的输入数据数组组成。该数组包含数据系列的 y 值。

清单 20-1。ch20_01.html

var data = [100, 110, 140, 130, 80, 75, 120, 130, 100];

清单 20-2 定义了一组与绘制图表的可视化尺寸相关的变量。wh变量是图表的宽度和高度;页边距用于在图表边缘留出空间。

清单 20-2。ch20_01.html

w = 400;

h = 300;

margin_x = 32;

margin_y = 20;

因为您在基于 x 轴和 y 轴的图形上工作,所以在 D3 中有必要为这两个轴中的每一个定义一个标度、一个域和一个值范围。我们先来理清这些概念,了解一下它们在 D3 中是如何管理的。

比例、域和范围

你已经不得不处理规模,即使你可能没有意识到这一点。线性标度更容易理解,尽管在一些例子中,你已经使用了对数标度(参见第 9 章中的侧栏“对数标度”)。标度只是将某个区间(称为域)中的值转换为属于另一个区间(称为范围)的另一个值的函数。但是这一切到底意味着什么呢?这对你有什么帮助?

实际上,每当您想要影响属于不同区间的两个变量之间的值的转换,但保持其相对于当前区间的“意义”时,这都可以为您服务。这涉及到规范化的概念。

假设您想要转换来自仪器的值,例如万用表报告的电压。您知道电压值应该在 0 到 5 伏之间,这是值的范围,也称为域。

您想要转换在红色标尺上测得的电压。使用红绿蓝(RGB)代码,该值将介于 0 和 255 之间。您现在已经定义了另一个颜色范围,即范围。

现在假设万用表上的电压读数是 2.7 伏,红色显示的色标对应的是 138(实际是 137.7)。您刚刚对值的转换应用了线性标度。图 20-1 显示了电压值到 RGB 刻度上相应 R 值的转换。这种转换在线性范围内进行,因为值是线性转换的。

A978-1-4302-6290-9_20_Fig1_HTML.jpg

图 20-1。

The conversion from the voltage to the R value is managed by the D3 library

但是这一切有什么用呢?首先,当您希望在图表中可视化数据时,不同间隔之间的转换并不罕见,其次,这种转换完全由 D3 库管理。你不需要做任何计算;您只需要定义要应用的域、范围和规模。

把这个例子翻译成 D3 代码,你可以写:

var scale = d3.scale.linear(),

.domain([0,5]),

.range([0,255]);

console.log(Math.round(scale(2.7)));      //it returns 138 on FireBug console

代码内部

可以定义规模、定义域、范围;因此,您可以通过在代码中添加清单 20-3 来继续实现折线图。

清单 20-3。ch20_01.html

y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);

x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);

因为输入数据数组是一维的,并且包含要在 y 轴上表示的值,所以可以将域从 0 扩展到数组的最大值。你不需要使用一个for循环来寻找这个值。D3 提供了一个名为max(date)的特定函数,其中传递的参数是要在其中找到最大值的数组。

现在是开始添加 SVG 元素的时候了。要添加的第一个元素是<svg>元素,它表示您要添加的所有其他元素的根。<svg>标签的功能有点类似于 jQuery 和 jqPlot 中画布的功能。因此,你需要用wh来指定画布的大小。在<svg>元素内部,您添加了一个<g>元素,这样所有内部添加到它的元素将被组合在一起。

随后,对这组<g>元素进行转换。在这种情况下,转换包括坐标网格的平移,向下移动h个像素,如清单 20-4 所示。

清单 20-4。ch20_01.html

var svg = d3.select("body")

.append("svg:svg")

.attr("width", w)

.attr("height", h);

var g = svg.append("svg:g")

.attr("transform", "translate(0," + h + ")");

创建折线图的另一个基本要素是路径元素。该路径由使用d属性的数据填充。

手动输入所有这些值太麻烦了,在这方面,D3 提供了一个函数来帮您完成这个任务:d3.svg.line。因此,在清单 20-5 中,你声明了一个名为line的变量,其中的所有数据都被转换成一个点(x,y)。

清单 20-5。ch20_01.html

var line = d3.svg.line()

.x(function(d,i) { return x(i); })

.y(function(d) { return -1 * y(d); });

正如你将看到的,在所有需要扫描数组的情况下(一个for循环),在 D3 里,这样的扫描通过使用参数di被不同地处理。数组当前项的索引用i表示,而当前项用d表示。回想一下,您通过变换将 y 轴向下平移。你需要保持这种心态;如果要正确画线,必须使用 y 的负值,这就是为什么要将d值乘以-1。

下一步是给一个path元素分配一行(见清单 20-6)。

清单 20-6。ch20_01.html

g.append("svg:path").attr("d", line(data));

如果您在这里停下来并启动页面上的 web 浏览器,您将得到如图 20-2 所示的图像。

A978-1-4302-6290-9_20_Fig2_HTML.jpg

图 20-2。

The default behavior of an SVG path element is to draw filled areas

这似乎是错误的,但是您必须考虑到在使用 SVG 创建图像时,由 CSS 样式管理的角色是占优势的。事实上,你可以简单地添加清单 20-7 中的 CSS 类来获得数据行。

清单 20-7。ch20_01.html

<style>

path {

stroke: steelblue;

stroke-width: 3;

fill: none;

}

line {

stroke: black;

}

</style>

因此,适当定义 CSS 样式类后,你会得到如图 20-3 所示的一行。

A978-1-4302-6290-9_20_Fig3_HTML.jpg

图 20-3。

The SVG path element draws a line if the CSS style classes are suitably defined

但是你还远没有一个折线图。您必须将两个轴相加。要绘制这两个对象,可以使用简单的 SVG 线条,如清单 20-8 所示。

清单 20-8。ch20_01.html

// draw the x axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(w))

.attr("y2", -y(0))

// draw the y axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(0))

.attr("y2", -y(d3.max(data))-10)

现在是添加标签的时候了。为此,有一个大大简化工作的 D3 函数:ticks()。此函数应用于 D3 刻度,如 x 或 y,并返回四舍五入后的数字作为刻度。您需要使用函数text(String)来获得当前d的字符串值(参见清单 20-9)。

清单 20-9。ch20_01.html

//draw the xLabels

g.selectAll(".xLabel")

.data(x.ticks(5))

.enter().append("svg:text")

.attr("class", "xLabel")

.text(String)

.attr("x", function(d) { return x(d) })

.attr("y", 0)

.attr("text-anchor", "middle");

// draw the yLabels

g.selectAll(".yLabel")

.data(y.ticks(5))

.enter().append("svg:text")

.attr("class", "yLabel")

.text(String)

.attr("x", 25)

.attr("y", function(d) { return -y(d) })

.attr("text-anchor", "end");

为了对齐标签,您需要指定属性text-anchor。它的可能值是middlestartend,这取决于您希望标签分别居中对齐、向左对齐还是向右对齐。

这里,您使用 D3 函数attr()来指定属性,但是也可以在 CSS 样式中指定它,如清单 20-10 所示。

清单 20-10。ch20_01.html

.xLabel {

text-anchor: middle;

}

.yLabel {

text-anchor: end;

}

事实上,写这几行几乎是一回事。然而,通常,当您计划更改这些值时,您会更喜欢在 CSS 样式中设置它们——它们被理解为参数。相反,在这种情况下,或者如果您希望它们是一个对象的固定属性,最好使用attr()函数来插入它们。

现在,您可以将记号添加到轴上。这是通过为每个刻度画一条短线来获得的。您对刻度标签所做的事情现在也同样适用于刻度,如清单 20-11 所示。

清单 20-11。ch20_01.html

//draw the x ticks

g.selectAll(".xTicks")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xTicks")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(0))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(0)-5)

// draw the y ticks

g.selectAll(".yTicks")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yTicks")

.attr("y1", function(d) { return -y(d); })

.attr("x1", x(0)+5)

.attr("y2", function(d) { return -y(d); })

.attr("x2", x(0))

20-4 为该阶段的折线图。

A978-1-4302-6290-9_20_Fig4_HTML.jpg

图 20-4。

Adding the two axes and the labels on them, you finally get a simple line chart

如您所见,您已经有了一个折线图。也许通过添加一个网格,如清单 20-12 所示,你可以让事情看起来更好。

清单 20-12。ch20_01.html

//draw the x grid

g.selectAll(".xGrids")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xGrids")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(0))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(d3.max(data))-10);

// draw the y grid

g.selectAll(".yGrids")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yGrids")

.attr("y1", function(d) { return -y(d); })

.attr("x1", x(w))

.attr("y2", function(d) { return -y(d); })

.attr("x2", x(0));

你可以对 CSS 样式做一些小的添加(见清单 20-13 ),以得到一个浅灰色的网格作为折线图的背景。此外,您可以定义更合适的文本样式,例如选择 Verdana 作为字体,大小为 9。

清单 20-13。ch20_01.html

<style>

path {

stroke: steelblue;

stroke-width: 3;

fill: none;

}

line {

stroke: black;

}

.xGrids {

stroke: lightgray;

}

.yGrids {

stroke: lightgray;

}

text {

font-family: Verdana;

font-size: 9pt;

}

</style>

现在用浅灰色网格绘制折线图,如图 20-5 所示。

A978-1-4302-6290-9_20_Fig5_HTML.jpg

图 20-5。

A line chart with a grid covering the blue lines

仔细看图 20-5 。网格的灰色线绘制在代表数据的蓝色线上方。换句话说,更明确地说,您必须注意绘制 SVG 元素的顺序。事实上,首先绘制轴和网格,然后最终移动到输入数据的表示上是很方便的。因此,你需要把你想画的所有项目按正确的顺序排列,如清单 20-14 所示。

清单 20-14。ch20_01.html

<script>

var data = [100,110,140,130,80,75,120,130,100];

w = 400;

h = 300;

margin_x = 32;

margin_y = 20;

y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);

x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);

var svg = d3.select("body")

.append("svg:svg")

.attr("width", w)

.attr("height", h);

var g = svg.append("svg:g")

.attr("transform", "translate(0," + h + ")");

var line = d3.svg.line()

.x(function(d,i) { return x(i); })

.y(function(d) { return -y(d); });

// draw the y axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(w))

.attr("y2", -y(0));

// draw the x axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(0))

.attr("y2", -y(d3.max(data))-10);

//draw the xLabels

g.selectAll(".xLabel")

.data(x.ticks(5))

.enter().append("svg:text")

.attr("class", "xLabel")

.text(String)

.attr("x", function(d) { return x(d) })

.attr("y", 0)

.attr("text-anchor", "middle");

// draw the yLabels

g.selectAll(".yLabel")

.data(y.ticks(5))

.enter().append("svg:text")

.attr("class", "yLabel")

.text(String)

.attr("x", 25)

.attr("y", function(d) { return -y(d) })

.attr("text-anchor", "end");

//draw the x ticks

g.selectAll(".xTicks")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xTicks")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(0))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(0)-5);

// draw the y ticks

g.selectAll(".yTicks")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yTicks")

.attr("y1", function(d) { return -1 * y(d); })

.attr("x1", x(0)+5)

.attr("y2", function(d) { return -1 * y(d); })

.attr("x2", x(0));

//draw the x grid

g.selectAll(".xGrids")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xGrids")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(0))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(d3.max(data))-10);

// draw the y grid

g.selectAll(".yGrids")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yGrids")

.attr("y1", function(d) { return -1 * y(d); })

.attr("x1", x(w))

.attr("y2", function(d) { return -y(d); })

.attr("x2", x(0));

// draw the x axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(w))

.attr("y2", -y(0));

// draw the y axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(0))

.attr("y2", -y(d3.max(data))+10);

// draw the line of data points

g.append("svg:path").attr("d", line(data));

</script>

20-6 显示了以正确顺序绘制的元素的折线图。事实上,代表输入数据的蓝线现在位于覆盖网格的前景上,而不是相反。

A978-1-4302-6290-9_20_Fig6_HTML.jpg

图 20-6。

A line chart with a grid drawn correctly

使用具有(x,y)值的数据

到目前为止,您已经使用了一个仅包含 y 值的输入数据数组。通常,您会想要表示分配了 x 和 y 值的点。因此,您将使用清单 20-15 中的输入数据数组来扩展前一种情况。

清单 20-15。ch20_02.html

var data = [{x: 0, y: 100}, {x: 10, y: 110}, {x: 20, y: 140},

{x: 30, y: 130}, {x: 40, y: 80}, {x: 50, y: 75},

{x: 60, y: 120}, {x: 70, y: 130}, {x: 80, y: 100}];

现在您可以看到数据是如何用包含 x 和 y 值的点来表示的。当您使用一个数据序列时,您通常需要立即确定 x 和 y 的最大值(有时还有最小值)。在前一个例子中,您使用了d3.maxd3.min函数,但是这些函数只对数组起作用,对对象不起作用。您插入的输入数据数组是一个对象数组。这个怎么解决?有几种方法。也许最直接的方法是扫描数据,找出 x 和 y 的最大值。在清单 20-16 中,你定义了两个包含这两个最大值的变量。然后一次扫描每个物体的 x 和 y 的值,你将 x 和 y 的当前值与xMaxyMax的值进行比较,看哪个值更大。两者中较大的将成为新的最大值。

清单 20-16。ch20_02.html

var xMax = 0, yMax = 0;

data.forEach(function(d) {

if(d.x > xMax)

xMax =  d.x;

if(d.y > yMax)

yMax =  d.y;

});

有几个有用的 D3 函数可以处理数组,那么为什么不直接从对象的输入数组创建两个数组呢——一个包含 x 的值,另一个包含 y 的值。你可以在任何需要的时候使用这两个数组,而不是使用对象数组,后者要复杂得多(见清单 20-17)。

清单 20-17。ch20_02.html

var ax = [];

var ay = [];

data.forEach(function(d,i){

ax[i] = d.x;

ay[i] = d.y;

})

var xMax = d3.max(ax);

var yMax = d3.max(ay);

这一次你把 x 和 y 都分配给数据点行,如清单 20-18 所示。即使在处理一组对象时,这个操作也非常简单。

清单 20-18。ch20_02.html

var line = d3.svg.line()

.x(function(d) { return x(d.x); })

.y(function(d) { return -y(d.y); })

至于代码的其余部分,没有太多要修改的——只有对 x 和 y 边界值的一些修正,如清单 20-19 所示。

清单 20-19。ch20_02.html

y = d3.scale.linear().domain([0,``yMaxT2】

x = d3.scale.linear().domain([0,``xMaxT2】

...

// draw the y axis

g.append("svg:line")

.attr("x1", x(0))

.attr("y1", -y(0))

.attr("x2", x(0))

.attr("y2", -y(yMax)-20)

...

//draw the x grid

g.selectAll(".xGrids")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xGrids")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(0))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(yMax)-10)

// draw the y grid

g.selectAll(".yGrids")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yGrids")

.attr("y1", function(d) { return -1 * y(d); })

.attr("x1", x(xMax)+20)

.attr("y2", function(d) { return -1 * y(d); })

.attr("x2", x(0))

20-7 显示了为处理输入数据数组引入的 y 值所做的更改的结果。

A978-1-4302-6290-9_20_Fig7_HTML.jpg

图 20-7。

A line chart with a grid and axis labels that take into account the y values entered with the input array

控制轴的范围

在您刚刚在代码中绘制的折线图中,数据行将始终位于图表的顶部。如果你的数据在很高的水平上波动,y 的刻度从 0 开始,你就有趋势线变平的风险。当 y 轴的上限是 y 的最大值时,这也不是最佳选择。在这里,您将添加对轴范围的检查。为此,在清单 20-20 中,您定义了四个变量来指定 x 轴和 y 轴的上限和下限。

清单 20-20。ch20_03.html

var xLowLim = 0;

var xUpLim = d3.max(ax);

var yUpLim = 1.2 * d3.max(ay);

var yLowLim = 0.8 * d3.min(ay);

因此,您可以用这些变量替换所有的限制引用。请注意,代码变得更加易读。以直接的方式指定这四个限制使您能够在需要时轻松地修改它们。在这种情况下,只显示了 y 轴上实验数据覆盖的范围,加上 20%的余量,如清单 20-21 所示。

清单 20-21。ch20_03.html

y = d3.scale.linear().domain([``yLowLim, yUpLimT2】

x = d3.scale.linear().domain([``xLowLim, xUpLimT2】

...

//draw the x ticks

g.selectAll(".xTicks")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xTicks")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(yLowLim))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(yLowLim)-5)

// draw the y ticks

g.selectAll(".yTicks")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yTicks")

.attr("y1", function(d) { return -y(d); })

.attr("x1", x(xLowLim))

.attr("y2", function(d) { return -y(d); })

.attr("x2", x(xLowLim)+5)

//draw the x grid

g.selectAll(".xGrids")

.data(x.ticks(5))

.enter().append("svg:line")

.attr("class", "xGrids")

.attr("x1", function(d) { return x(d); })

.attr("y1", -y(yLowLim))

.attr("x2", function(d) { return x(d); })

.attr("y2", -y(yUpLim))

// draw the y grid

g.selectAll(".yGrids")

.data(y.ticks(5))

.enter().append("svg:line")

.attr("class", "yGrids")

.attr("y1", function(d) { return -y(d); })

.attr("x1", x(xUpLim)+20)

.attr("y2", function(d) { return -y(d); })

.attr("x2", x(xLowLim))

// draw the x axis

g.append("svg:line")

.attr("x1", x(xLowLim))

.attr("y1", -y(yLowLim))

.attr("x2", 1.2*x(xUpLim))

.attr("y2", -y(yLowLim))

// draw the y axis

g.append("svg:line")

.attr("x1", x(xLowLim))

.attr("y1", -y(yLowLim))

.attr("x2", x(xLowLim))

.attr("y2", -1.2*y(yUpLim))

20-8 显示了 y 轴范围在 60°和 160°之间的新折线图,更好地显示了线条。

A978-1-4302-6290-9_20_Fig8_HTML.jpg

图 20-8。

A line chart with y-axis range focused around the y values

添加轴箭头

为了更好地理解 D3 的图形多功能性,特别是在新特性的实现中,您将学习向 x 轴和 y 轴添加箭头。要做到这一点,你必须在清单 20-22 中添加两条路径,因为它们会在两个轴的末端画出箭头。

清单 20-22。ch20_04.html

g.append("svg:path")

.attr("class", "axisArrow")

.attr("d", function() {

var x1 = x(xUpLim)+23, x2 = x(xUpLim)+30;

var y2 = -y(yLowLim),y1 = y2-3, y3 = y2+3

return 'M'+x1+','+y1+','+x2+','+y2+','+x1+','+y3;

});

g.append("svg:path")

.attr("class", "axisArrow")

.attr("d", function() {

var y1 = -y(yUpLim)-13, y2 = -y(yUpLim)-20;

var x2 = x(xLowLim),x1 = x2-3, x3 = x2+3

return 'M'+x1+','+y1+','+x2+','+y2+','+x3+','+y1;

});

在 CCS 风格中,您添加了axisArrow类,如清单 20-23 所示。您也可以选择启用fill属性来获得一个实心箭头。

清单 20-23。ch20_04.html

.axisArrow {

stroke: black;

stroke-width: 1;

/*fill: black; */

}

20-9 显示了填充和未填充的结果。

A978-1-4302-6290-9_20_Fig9_HTML.jpg

图 20-9。

Two different ways to represent the arrows on the axes

添加标题和轴标签

在本节中,您将向图表添加标题。这是一件非常简单的事情,您将使用名为text的 SVG 元素,并对样式进行适当的修改,如清单 20-24 所示。这段代码将把标题放在中间的顶部。

清单 20-24。ch20_05.html

g.append("svg:text")

.attr("x", (w / 2))

.attr("y", -h + margin_y )

.attr("text-anchor", "middle")

.style("font-size", "22px")

.text("My first D3 line chart");

20-10 显示了添加到折线图顶部的标题。

A978-1-4302-6290-9_20_Fig10_HTML.jpg

图 20-10。

A line chart with a title

按照类似的步骤,你也可以给轴添加标签(见清单 20-25)。

清单 20-25。ch20_05.html

g.append("svg:text")

.attr("x", 25)

.attr("y", -h + margin_y)

.attr("text-anchor", "end")

.style("font-size", "11px")

.text("[#]");

g.append("svg:text")

.attr("x", w - 40)

.attr("y", -8 )

.attr("text-anchor", "end")

.style("font-size", "11px")

.text("time [s]");

20-11 显示了放在相应轴旁边的两个新轴标签。

A978-1-4302-6290-9_20_Fig11_HTML.jpg

图 20-11。

A more complete line chart with title and axes labels

现在,您已经学会了如何制作折线图,您可以尝试一些更复杂的图表了。通常,要在图表中显示的数据不在网页中,而是在外部文件中。您将把以下关于如何从外部文件读取数据的课程与您目前所学的内容结合起来。

从 CSV 文件中的数据绘制折线图

设计图表时,通常会引用各种格式的数据。这些数据通常来自几个不同的来源。在最常见的情况下,您的服务器(您的 web 页面所指向的服务器)上有从数据库或通过检测提取数据的应用,或者您甚至可能有在这些服务器上收集的数据文件。这里的示例使用位于服务器上的逗号分隔值(CSV)文件作为数据源。这个 CSV 文件包含数据,可以直接加载到服务器上,也可以由其他应用生成。

D3 已经准备好处理这种类型的文件,这不是巧合。为此,D3 提供了函数d3.csv()。您将通过一个示例了解关于这个主题的更多信息。

读取和解析数据

首先,您需要定义“画布”的大小,或者更好地定义您想要绘制图表的区域的大小和边距。这一次,您定义了四个边距。这将使你对绘图区域有更多的控制(见清单 20-26)。

清单 20-26。ch20_06a.html

<!DOCTYPE html>

<meta charset="utf-8">

<style>

</style>

<body>

<script src="http://d3js.org/d3.v3.jsT2】

<script>

var margin = {top: 70, right: 20, bottom: 30, left: 50},

w = 400 - margin.left - margin.right,

h = 400 - margin.top - margin.bottom;

现在你处理数据;用文本编辑器将清单 20-27 中的数据写入一个文件,并另存为data_01.csv

清单 20-27。data_01.csv

date,attendee

12-Feb-12,80

27-Feb-12,56

02-Mar-12,42

14-Mar-12,63

30-Mar-12,64

07-Apr-12,72

18-Apr-12,65

02-May-12,80

19-May-12,76

28-May-12,66

03-Jun-12,64

18-Jun-12,53

29-Jun-12,59

该数据包含由逗号分隔的两组值(回想一下 CSV 代表逗号分隔值)。第一个是日期格式,列出发生特定事件的日期,例如会议。第二列列出了与会者的人数。请注意,日期没有用引号括起来。

以类似 jqPlot 的方式,D3 有许多控制时间格式化的工具。事实上,要处理 CSV 文件中包含的日期,您必须指定一个解析器,如清单 20-28 所示。

清单 20-28。ch20_06a.html

var parseDate = d3.time.format("%d-%b-%y").parse;

这里需要指定 CSV 文件中包含的格式:%d表示天数的数字格式,%b表示前三个字符表示上报的月份,%y表示后两位数字表示上报的年份。您可以指定 x 和 y 值,给它们指定一个比例和范围,如清单 20-29 所示。

清单 20-29。ch20_06a.html

var x = d3.time.scale().range([0, w]);

var y = d3.scale.linear().range([h, 0]);

现在您已经处理了输入数据的正确处理,您可以开始创建图形组件了。

实现轴和网格

你将从学习如何图形化地实现两个笛卡尔轴开始。在这个例子中,如清单 20-30 所示,你按照最合适的方式通过函数d3.svg.axis()指定 x 轴和 y 轴。

清单 20-30。ch20_06a.html

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5);

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5);

这使您可以专注于数据,而所有与轴相关的问题(记号、标签等)都由轴组件自动处理。因此,在您创建了xAxisyAxis之后,您将 x 和 y 的比例分配给它们并设置方向。简单吗?有;这一次,您不必指定所有关于轴的繁琐内容——它们的限制,在哪里放置刻度和标签,等等。与前面的例子不同,所有这些都是用很少的几行自动完成的。我选择现在引入这个概念,因为在前面的例子中,我想强调这样一个事实,即你设计的每一个项目都是你可以用 D3 管理的一块砖,不管这个过程是否在 D3 库中自动化。

现在您可以将 SVG 元素添加到页面中,如清单 20-31 所示。

清单 20-31。ch20_06a.html

var svg = d3.select("body").append("svg")

.attr("width", w + margin.left + margin.right)

.attr("height", h + margin.top + margin.bottom)

.append("g")

.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

svg.append("g")

.attr("class", "x axis")

.attr("transform", "translate(0," + h + ")")

.call(xAxis);

svg.append("g")

.attr("class", "y axis")

.call(yAxis);

注意 x 轴是如何被平移的。事实上,在没有规范的情况下,x 轴将被绘制在绘图区域的顶部。而且,你还需要添加 CSS 样式。请参见清单 20-32。

清单 20-32。ch20_06a.html

<style>

body {

font: 10px verdana;

}

.axis path,

.axis line {

fill: none;

stroke: #333;

}

</style>

20-12 显示了结果。

A978-1-4302-6290-9_20_Fig12_HTML.jpg

图 20-12。

An empty chart ready to be filled with data

使用 FireBug,您可以看到刚刚定义的 SVG 元素的结构(参见图 20-13 )。

A978-1-4302-6290-9_20_Fig13_HTML.jpg

图 20-13。

FireBug shows the structure of the SVG elements created dynamically to display the axes

您可以看到所有的元素都被自动分组到组<g>标签中。这使您能够更好地将可能的转换应用到单独的元素。

如果需要,您也可以添加网格。你建立网格的方法和建立轴的方法一样。事实上,以同样的方式,您使用清单 20-33 中的axis()函数定义了两个网格变量——xGridyGrid

清单 20-33。ch20_06a.html

var yAxis = d3.svg.axis()

...

var xGrid = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5)

.tickSize(-h, 0, 0)

.tickFormat("");

var yGrid = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5)

.tickSize(-w, 0, 0)

.tickFormat("");

在 JavaScript 代码的底部,将两个新的 SVG 元素添加到另一个中,如清单 20-34 所示。

清单 20-34。ch20_06a.html

svg.append("g")

.attr("class", "y axis")

.call(yAxis)

svg.append("g")

.attr("class", "grid")

.attr("transform", "translate(0," + h + ")")

.call(xGrid);

svg.append("g")

.attr("class", "grid")

.call(yGrid);

这两个元素使用相同的类名命名:grid。因此,你可以把它们作为一个单独的元素来设计(见清单 20-35)。

清单 20-35。ch20_06a.html

<style>

...

.grid .tick {

stroke: lightgrey;

opacity: 0.7;

}

.grid path {

stroke-width: 0;

}

</style>

20-14 显示了您刚刚定义为 SVG 元素的水平网格线。

A978-1-4302-6290-9_20_Fig14_HTML.jpg

图 20-14。

Beginning to draw the horizontal grid lines

您的图表现在可以显示 CSV 文件中的数据了。

用 csv()函数绘制数据

现在是时候在图表中显示数据了,你可以用 D3 函数d3.csv()来完成,如清单 20-36 所示。

清单 20-36。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

// Here we will put all the SVG elements affected by the data

// on the file!!!

});

第一个参数是 CSV 文件的名称;第二个参数是处理文件中所有数据的函数。所有以某种方式受这些值影响的 D3 函数都必须放在这个函数中。例如,您使用svg.append()来创建新的 SVG 元素,但是其中许多函数需要知道数据的 x 和 y 值。所以你需要把它们放在csv()函数中作为第二个参数。

CSV 文件中的所有数据都收集在一个名为data的对象中。CSV 文件的不同字段通过它们的标题来识别。您要添加的第一件事是一个迭代函数,其中的data对象被逐项读取。在这里,解析日期值。您必须确保所有与会者值都被读取为数字(这可以通过在每个值前面加上一个加号来实现)。请参见清单 20-37。

清单 20-37。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(function(d) {

d.date = parseDate(d.date);

d.attendee = +d.attendee;

});

});

只有现在才有可能在清单 20-38 中定义 x 和 y 上的定义域,因为只有现在你才知道这些数据的值。

清单 20-38。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(函数(d) {

...

});

x.domain(d3.extent(data, function(d) { return d.date; }));

y.domain(d3.extent(data, function(d) { return d.attendee; }));

});

一旦从文件中读取并收集了数据,它就构成了一组必须用线连接的点(x,y)。您将使用 SVG 元素path来构建这一行,如清单 20-39 所示。正如您之前看到的,函数d3.svg.line()使工作变得更加容易。

清单 20-39。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(函数(d) {

...

});

...

var line = d3.svg.line()

.x(function(d) { return x(d.date); })

.y(function(d) { return y(d.attendee); });

});

您还可以向图表添加两个轴标签和一个标题。这是一个如何手动构建<g>组的好例子。以前,组和其中的所有元素都是由函数创建的;现在你需要明确地做到这一点。如果你想给一个组添加两个轴标签,给另一个组添加标题,你需要指定两个不同的变量:labelstitle(见清单 20-40)。

清单 20-40。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(函数(d) {

...

});

...

var labels = svg.append("g")

.attr("class","labels")

labels.append("text")

.attr("transform", "translate(0," + h + ")")

.attr("x", (w-margin.right))

.attr("dx", "-1.0em")

.attr("dy", "2.0em")

.text("[Months]");

labels.append("text")

.attr("transform", "rotate(-90)")

.attr("y", -40)

.attr("dy", ".71em")

.style("text-anchor", "end")

.text("Attendees");

var title = svg.append("g")

.attr("class","title");

title.append("text")

.attr("x", (w / 2))

.attr("y", -30 )

.attr("text-anchor", "middle")

.style("font-size", "22px")

.text("A D3 line chart from CSV file");

});

在每种情况下,都用append()方法创建一个 SVG 元素<g>,并用一个类名定义这个组。随后,通过对这些变量使用append(),将 SVG 元素分配给这两个组。

最后,你可以添加path元素,它画出代表数据值的线(见清单 20-41)。

清单 20-41。ch20_06a.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(function(d) {

...

});

...

svg.append("path")

.datum(data)

.attr("class", "line")

.attr("d", line);

});

即使对于这个新的 SVG 元素,您也不能忘记添加它的 CSS 样式设置,如清单 20-42 所示。

清单 20-42。ch20_06a.html

<style>

...

.line {

fill: none;

stroke: steelblue;

stroke-width: 1.5px;

}

</style>

20-15 显示了报告 CSV 文件中所有数据的漂亮折线图。

A978-1-4302-6290-9_20_Fig15_HTML.jpg

图 20-15。

A complete line chart with all of its main components

向线添加标记

正如您在 jqPlot 的折线图中看到的,即使在这里也可以进行进一步的添加。例如,您可以在线上放置数据标记。

在所有添加的 SVG 元素末尾的d3.csv()函数中,你可以添加标记(见清单 20-43)。记住这些元素依赖于数据,所以它们必须被插入到csv()函数中。

清单 20-43。ch20_06b.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(函数(d) {

...

});

...

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 3.5)

.attr("cx", function(d) { return x(d.date); })

.attr("cy", function(d) { return y(d.attendee); });

});

在文件的样式部分,添加清单 20-44 中的.dot类的 CSS 样式定义。

清单 20-44。ch20_06b.html

.dot {

stroke: steelblue;

fill: lightblue;

}

20-16 为小圆圈标记的折线图;这个结果与用 jqPlot 库得到的结果非常相似。

A978-1-4302-6290-9_20_Fig16_HTML.jpg

图 20-16。

A complete line chart with markers

这些标记是圆形的,但是也可以是其他的形状和颜色。例如,你可以使用正方形的标记(见清单 20-45)。

清单 20-45。ch20_06c.html

<style>

.dot {

stroke: darkred;

fill: red;

}

</style>

...

svg.selectAll(".dot")

.data(data)

.enter().append("rect")

.attr("class", "dot")

.attr("width", 7)

.attr("height", 7)

.attr("x", function(d) { return x(d.date)-3.5; })

.attr("y", function(d) { return y(d.attendee)-3.5; });

20-17 显示了相同的折线图,但这次它使用红色小方块作为标记。

A978-1-4302-6290-9_20_Fig17_HTML.jpg

图 20-17。

One of the many marker options

你也可以使用黄色菱形的标记,通常被称为菱形(见清单 20-46)。

清单 20-46。ch20_06d.html

<style>

.dot {

stroke: orange;

fill: yellow;

}

</style>

...

svg.selectAll(".dot")

.data(data)

.enter().append("rect")

.attr("class", "dot")

.attr("transform", function(d) {

var str = "rotate(45," + x(d.date) + "," + y(d.attendee) + ")";

return str;

})

.attr("width", 7)

.attr("height", 7)

.attr("x", function(d) { return x(d.date)-3.5; })

.attr("y", function(d) { return y(d.attendee)-3.5; });

20-18 显示了黄色菱形的标记。

A978-1-4302-6290-9_20_Fig18_HTML.jpg

图 20-18。

Another marker option

带填充区域的折线图

在本节中,您将把点标记放在一边,并返回到基本折线图。您可以添加到图表中的另一个有趣的功能是填充线条下方的区域。还记得d3.svg.line()功能吗?嗯,这里你用的是d3.svg.area()函数。就像 D3 里有一个line对象一样,你也有一个area对象。因此,要定义一个area对象,你可以将清单 20-47 中粗体显示的行添加到代码中,就在line对象定义的下面。

清单 20-47。ch20_07.html

var line = d3.svg.line()

.x(function(d) { return x(d.date); })

.y(function(d) { return y(d.attendee); });

var area = d3.svg.area()

.x(function(d) { return x(d.date); })

.y0(h)

.y1(function(d) { return y(d.attendee); });

var labels = svg.append("g")

...

如您所见,要定义一个区域,您需要指定三个界定边缘的函数:xy0y1。在这种情况下,y0是常数,对应于绘图区域的底部(x 轴)。现在您需要在 SVG 中创建相应的元素,它由一个path元素表示,如清单 20-48 所示。

清单 20-48。ch20_07.html

d3.csv("data_01.csv", function(error, data) {

data.forEach(function(d) {

d.date = parseDate(d.date);

d.attendee = +d.attendee;

});

...

svg.append("path")

.datum(data)

.attr("class", "line")

.attr("d", line);

svg.append("path")

.datum(data)

.attr("class", "area")

.attr("d", area);

});

如清单 20-49 所示,您需要在相应的 CSS 样式类中指定颜色设置。

清单 20-49。ch20_07.html

.area {

fill: lightblue;

}

20-19 显示了从折线图派生的面积图。

A978-1-4302-6290-9_20_Fig19_HTML.jpg

图 20-19。

An area chart

多系列折线图

现在您已经熟悉了使用 SVG 元素创建折线图的基本组件,下一步是开始处理多个数据系列:多系列折线图。本节中最重要的元素是图例。您将学习通过利用 SVG 提供的基本图形元素来创建一个。

处理多个系列的数据

到目前为止,您一直在处理单个系列的数据。现在是转向多系列的时候了。在前面的示例中,您使用 CSV 文件作为数据源。现在,你将看到另一个 D3 函数:d3.tsv()。它执行与csv()相同的任务,但是操作制表符分隔值(TSV)文件。

将清单 20-50 复制到您的文本编辑器中,并保存为data_02.tsv(参见下面的注释)。

Note

TSV 文件中的值是用制表符分隔的,所以当您编写或复制清单 20-50 时,记得检查每个值之间只有一个制表符。

清单 20-50。data_02.tsv

Date    europa    asia    america

12-Feb-12    52    40    65

27-Feb-12    56    35    70

02-Mar-12    51    45    62

14-Mar-12    63    44    82

30-Mar-12    64    54    85

07-Apr-12    70    34    72

18-Apr-12    65    36    69

02-May-12    56    40    71

19-May-12    71    55    75

28-May-12    45    32    68

03-Jun-12    64    44    75

18-Jun-12    53    36    78

29-Jun-12    59    42    79

清单 20-50 有四列,其中第一列是日期,另外三列是来自不同大洲的值。第一列包含 x 值;其他的是三个系列对应的 y 值。

开始编写清单 20-51 中的代码;没有任何解释,因为这段代码实际上与上一个示例相同。

清单 20-51。ch20_08a.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<script src="http://d3js.org/d3.v3.jsT2】

<style>

body {

font: 10px verdana;

}

.axis path,

.axis line {

fill: none;

stroke: #333;

}

.grid .tick {

stroke: lightgrey;

opacity: 0.7;

}

.grid path {

stroke-width: 0;

}

.line {

fill: none;

stroke: steelblue;

stroke-width: 1.5px;

}

</style>

</head>

<body>

<script type="text/javascript">

var margin = {top: 70, right: 20, bottom: 30, left: 50},

w = 400 - margin.left - margin.right,

h = 400 - margin.top - margin.bottom;

var parseDate = d3.time.format("%d-%b-%y").parse;

var x = d3.time.scale().range([0, w]);

var y = d3.scale.linear().range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5);

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5);

var xGrid = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5)

.tickSize(-h, 0, 0)

.tickFormat("");

var yGrid = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5)

.tickSize(-w, 0, 0)

.tickFormat("");

var svg = d3.select("body").append("svg")

.attr("width", w + margin.left + margin.right)

.attr("height", h + margin.top + margin.bottom)

.append("g")

.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var line = d3.svg.line()

.x(function(d) { return x(d.date); })

.y(function(d) { return y(d.attendee); });

// Here we add the d3.tsv function

// start of the part of code to include in the d3.tsv() function

d3.tsv("data_02.tsv ",函数(error,data) {

svg.append("g")

.attr("class", "x axis")

.attr("transform", "translate(0," + h + ")")

.call(xAxis);

svg.append("g")

.attr("class", "y axis")

.call(yAxis);

svg.append("g")

.attr("class", "grid")

.attr("transform", "translate(0," + h + ")")

.call(xGrid);

svg.append("g")

.attr("class", "grid")

.call(yGrid);

});

//end of the part of code to include in the d3.tsv() function

var labels = svg.append("g")

.attr("class","labels");

labels.append("text")

.attr("transform", "translate(0," + h + ")")

.attr("x", (w-margin.right))

.attr("dx", "-1.0em")

.attr("dy", "2.0em")

.text("[Months]");

labels.append("text")

.attr("transform", "rotate(-90)")

.attr("y", -40)

.attr("dy", ".71em")

.style("text-anchor", "end")

.text("Attendees");

var title = svg.append("g")

.attr("class","title");

title.append("text")

.attr("x", (w / 2))

.attr("y", -30 )

.attr("text-anchor", "middle")

.style("font-size", "22px")

.text("A multiseries line chart");

</script>

</body>

</html>

当您在单个图表中处理多系列数据时,您需要能够快速识别数据,因此您需要使用不同的颜色。D3 提供了一些生成已经定义的颜色序列的函数。例如,有一个category10()函数,它提供了 10 种不同颜色的序列。您可以通过编写清单 20-52 中的线条来为多系列折线图创建一个颜色集。

清单 20-52。ch20_08a.html

...

var x = d3.time.scale().range([0, w]);

var y = d3.scale.linear().range([h, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()

...

您现在需要读取 TSV 文件中的数据。和前面的例子一样,在调用了d3.tsv()函数之后,你添加了一个解析器,如清单 20-53 所示。因为必须处理 x 轴上的日期值,所以必须解析这种类型的值。您将使用parseDate()函数。

清单 20-53。ch20_08a.html

d3.tsv("data_02.tsv ",函数(error,data) {

data.forEach(function(d) {

d.date = parseDate(d.date);

});

...

});

您已经定义了一个颜色集,在带有scale()函数的链中使用了category10()函数。这意味着 D3 将颜色序列作为一个标度来处理。您需要创建一个域,如清单 20-54 所示(在这种情况下,它将由离散值组成,而不是像 x 或 y 那样的连续值)。该域由 TSV 文件中的头组成。在这个例子中,你有三块大陆。因此,您将拥有一个包含三个值的属性域和一个包含三种颜色的颜色序列。

清单 20-54。ch20_08a.html

d3.tsv("data_02.tsv", function(error, data) {

data.forEach(function(d) {

d.date = parseDate(d.date);

});

color.domain(d3.keys(data[0]).filter(function(key) {

return key !== "date";

}));

...

});

在清单 20-53 中,你可以看到data[0]被作为参数传递给了d3.keys()函数。data[0]是 TSV 文件第一行对应的对象:

Object { date=Date {Sun Feb 12 2012 00:00:00 GMT+0100},

europa="52", asia="40", america="65"}.

d3.keys()函数从一个对象中提取值的名称,这个名称就是我们在 TSV 文件中发现的标题。所以使用d3.keys(data[0]),你得到了字符串数组:

["date","europa","asia","america"]

您只对最后三个值感兴趣,所以您需要过滤这个数组,以便排除键"date".,您可以使用filter()函数来这样做。最后,您将把三大洲指定给颜色域。

["europa","asia","america"]

清单 20-55 中的命令重组了结构化对象数组中的所有数据。这是由带有内部函数的函数map()完成的,它按照定义的结构映射值。

清单 20-55。ch20_08a.html

d3.tsv("data_02.tsv", function(error, data) {

...

color.domain(d3.keys(data[0]).filter(function(key) {

return key !== "date";

}));

var continents = color.domain().map(function(name) {

return {

name: name,

values: data.map(function(d) {

return {date: d.date, attendee: +d[name]};

})

};

});

...

});

这是由三个物体组成的阵列,叫做大陆。

[ Object { name="europa", values=[13]},

Object { name="asia", values=[13]},

Object { name="america", values=[13]} ]

每个对象都有一个洲名和一个由 13 个对象组成的值数组:

[ Object { date=Date, attendee=52 },

Object { date=Date, attendee=56 },

Object { date=Date, attendee=51 },

...]

您以一种允许后续处理的方式组织数据。事实上,当你需要指定图表的 y 域时,你可以通过两次迭代找到系列中所有值的最大值和最小值(不是每个单独的值)(见清单 20-56)。使用function(c),可以对所有的大陆进行迭代,使用function(v),可以对其中的所有值进行迭代。最终,d3.mind3.max将只提取一个值。

清单 20-56。ch20_08a.html

d3.tsv("data_02.tsv", function(error, data) {

...

var continents = color.domain().map(function(name) {

...

});

x.domain(d3.extent(data, function(d) { return d.date; }));

y.domain([

d3.min(continents, function(c) {

return d3.min(c.values, function(v) { return v.attendee; });

}),

d3.max(continents, function(c) {

return d3.max(c.values, function(v) { return v.attendee; });

})

]);

...

});

由于有了新的数据结构,您可以为每个包含一条线路径的大陆添加一个 SVG 元素<g>,如清单 20-57 所示。

清单 20-57。ch20_08a.html

d3.tsv("data_02.tsv", function(error, data) {

...

svg.append("g")

.attr("class", "grid")

.call(yGrid);

var continent = svg.selectAll(".continent")

.data(continents)

.enter().append("g")

.attr("class", "continent");

continent.append("path")

.attr("class", "line")

.attr("d", function(d) { return line(d.values); })

.style("stroke", function(d) { return color(d.name); });

});

由此产生的多系列折线图如图 20-20 所示。

A978-1-4302-6290-9_20_Fig20_HTML.jpg

图 20-20。

A multiseries line chart

添加图例

当您处理多序列图表时,下一个合乎逻辑的步骤是添加图例,以便用颜色和标签对序列进行分类。因为图例和其他任何图形对象一样,都是一个图形对象,所以您需要添加 SVG 元素来允许您在图表上绘制它(参见清单 20-58)。

清单 20-58。ch20_08a.html

d3.tsv("data_02.tsv", function(error, data) {

...

continent.append("path")

.attr("class", "line")

.attr("d", function(d) { return line(d.values); })

.style("stroke", function(d) { return color(d.name); });

var legend = svg.selectAll(".legend")

.data(color.domain().slice().reverse())

.enter().append("g")

.attr("class", "legend")

.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

legend.append("rect")

.attr("x", w - 18)

.attr("y", 4)

.attr("width", 10)

.attr("height", 10)

.style("fill", color);

legend.append("text")

.attr("x", w - 24)

.attr("y", 9)

.attr("dy", ".35em")

.style("text-anchor", "end")

.text(function(d) { return d; });

});

产生的多系列折线图如图 20-21 所示,带有图例。

A978-1-4302-6290-9_20_Fig21_HTML.jpg

图 20-21。

A multiseries line chart with a legend

插值线

你还记得用 jqPlot 库处理多系列折线图时线条的平滑效果吗?(如果没有,可以在第九章的“平滑折线图”部分找到。)在折线图中,您通常将数据点按顺序一个接一个地连接成一条直线。您还看到了如何将这些点连接成一条曲线。事实上,这种效果是通过插值得到的。从数学的角度来看,D3 库以更正确的方式覆盖了数据点的插值。因此,你需要更深入地研究这个概念。

当您有一组值并希望用折线图来表示它们时,您实际上希望了解这些值所代表的趋势。根据这一趋势,您可以评估在一个数据点和下一个数据点之间的中间点可以获得哪些值。有了这样的估计,你实际上影响了插值。根据趋势和想要达到的精确度,您可以使用各种数学方法来调整连接数据点的曲线形状。

最常用的方法是样条。(如果你想加深对题目的了解,请访问 http://paulbourke.net/miscellaneous/interpolation/ 。)表 20-1 列出了 D3 库提供的各种插值类型。

表 20-1。

The options for interpolating lines available within the D3 library

| 选择 | 描述 | | --- | --- | | basis | 一条 B 样条曲线,两端有重复的控制点。 | | basis-open | 一个开放的 B 样条;不得与起点或终点相交。 | | basis-closed | 闭合的 B 样条,如在循环中。 | | bundle | 等同于basis,除了张力参数用于拉直花键。 | | cardinal | 基数样条,两端有控制点副本。 | | cardinal-open | 开基数样条;可能不会与起点或终点相交,但会与其他控制点相交。 | | cardinal-closed | 闭合基数样条,如在环中。 | | Linear | 分段线性线段,如在折线中。 | | linear-closed | 闭合线性线段以形成多边形。 | | monotone | 保持 y 方向单调效果的三次插值。 | | step-before | 在垂直段和水平段之间交替,如在阶跃函数中。 | | step-after | 在水平段和垂直段之间交替,如在阶跃函数中。 |

You find these options by visiting https://github.com/mbostock/d3/wiki/SVG-Shapes#wiki-line_interpolate .

现在你更好地理解了什么是插值,你可以看到一个实际的例子。在前面的示例中,有三个系列由不同颜色的线表示,并由连接数据点(x,y)的线段组成。但是也可以绘制相应的插值线。

如清单 20-59 所示,您只需将interpolate()方法添加到d3.svg.line中就可以获得想要的效果。

清单 20-59。ch20_08b.html

var line = d3.svg.line()

.interpolate("basis")

.x(function(d) { return x(d.date); })

.y(function(d) { return y(d.attendee); });

20-22 显示了应用于图表中三个系列的插值线。连接数据点的直线已被曲线取代。

A978-1-4302-6290-9_20_Fig22_HTML.jpg

图 20-22。

A smooth multiseries line chart

差异折线图

这种图表描绘了两个系列之间的区域。在第一个系列大于第二个系列的范围内,该区域具有一种颜色,在第一个系列小于第二个系列的范围内,该区域具有不同的颜色。这种图表的一个很好的例子是比较收入和支出随时间变化的趋势。当收入大于支出时,该区域将是绿色的(通常绿色代表 OK),而当收入小于支出时,该区域是红色的(意味着不好)。将清单 20-60 中的值写入一个 TSV(或 CSV)文件,并将其命名为data_03.tsv(见注释)。

Note

TSV 文件中的值是用制表符分隔的,所以当您编写或复制清单 20-60 时,记得检查每个值之间只有一个制表符。

清单 20-60。data_03.tsv

Date    income    expense

12-Feb-12    52    40

27-Feb-12    56    35

02-Mar-12    31    45

14-Mar-12    33    44

30-Mar-12    44    54

07-Apr-12    50    34

18-Apr-12    65    36

02-May-12    56    40

19-May-12    41    56

28-May-12    45    32

03-Jun-12    54    44

18-Jun-12    43    46

29-Jun-12    39    52

开始编写清单 20-61 中的代码;这次不包括解释,因为这个例子实际上与上一个一样。

清单 20-61。ch20_09.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<script src="http://d3js.org/d3.v3.jsT2】

<style>

body {

font: 10px verdana;

}

.axis path,

.axis line {

fill: none;

stroke: #333;

}

.grid .tick {

stroke: lightgrey;

opacity: 0.7;

}

.grid path {

stroke-width: 0;

}

</style>

</head>

<body>

<script type="text/javascript">

var margin = {top: 70, right: 20, bottom: 30, left: 50},

w = 400 - margin.left - margin.right,

h = 400 - margin.top - margin.bottom;

var parseDate = d3.time.format("%d-%b-%y").parse;

var x = d3.time.scale().range([0, w]);

var y = d3.scale.linear().range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5);

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5);

var xGrid = d3.svg.axis()

.scale(x)

.orient("bottom")

.ticks(5)

.tickSize(-h, 0, 0)

.tickFormat("");

var yGrid = d3.svg.axis()

.scale(y)

.orient("left")

.ticks(5)

.tickSize(-w, 0, 0)

.tickFormat("");

var svg = d3.select("body").append("svg")

.attr("width", w + margin.left + margin.right)

.attr("height", h + margin.top + margin.bottom)

.append("g")

.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Here we add the d3.tsv function

// start of the part of code to include in the d3.tsv() function

d3.tsv("data_03.tsv", function(error, data) {

svg.append("g")

.attr("class", "x axis")

.attr("transform", "translate(0," + h + ")")

.call(xAxis);

svg.append("g")

.attr("class", "y axis")

.call(yAxis);

svg.append("g")

.attr("class", "grid")

.attr("transform", "translate(0," + h + ")")

.call(xGrid);

svg.append("g")

.attr("class", "grid")

.call(yGrid);

});

//end of the part of code to include in the d3.tsv() function

var labels = svg.append("g")

.attr("class","labels");

labels.append("text")

.attr("transform", "translate(0," + h + ")")

.attr("x", (w-margin.right))

.attr("dx", "-1.0em")

.attr("dy", "2.0em")

.text("[Months]");

labels.append("text")

.attr("transform", "rotate(-90)")

.attr("y", -40)

.attr("dy", ".71em")

.style("text-anchor", "end")

.text("Millions ($)");

var title = svg.append("g")

.attr("class","title");

title.append("text")

.attr("x", (w / 2))

.attr("y", -30 )

.attr("text-anchor", "middle")

.style("font-size", "22px")

.text("A difference chart");

</script>

</body>

</html>

首先,您阅读 TSV 文件,检查收入和费用值是否为正。然后解析所有的日期值(见清单 20-62)。

清单 20-62。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

data.forEach(function(d) {

d.date = parseDate(d.date);

d.income = +d.income;

d.expense = +d.expense;

});

...

});

这里,不像前面的例子(多系列折线图),不需要重新构造数据,所以你可以在 x 和 y 上创建一个域,如清单 20-63 所示。用Math.maxMath.min比较每一步的收入和费用值,然后用d3.mind3.max找出影响每一步迭代的值,得到最大值和最小值。

清单 20-63。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

data.forEach(function(d) {

...

});

x.domain(d3.extent(data, function(d) { return d.date; }));

y.domain([

d3.min(data, function(d) {return Math.min(d.income, d.expense); }),

d3.max(data, function(d) {return Math.max(d.income, d.expense); })

]);

...

});

在添加 SVG 元素之前,您需要定义一些 CSS 类。当支出大于收入时,你会用红色,否则用绿色。你需要定义这些颜色,如清单 20-64 所示。

清单 20-64。ch20_09.html

<style>

...

.area.above {

fill: darkred;

}

.area.below {

fill: lightgreen;

}

.line {

fill: none;

stroke: #000;

stroke-width: 1.5px;

}

</style>

因为你需要表示线条和区域,你可以通过数据点之间的插值来定义它们(见清单 20-65)。

清单 20-65。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

...

svg.append("g")

.attr("class", "grid")

.call(yGrid);

var line = d3.svg.area()

.interpolate("basis")

.x(function(d) { return x(d.date); })

.y(function(d) { return y(d["income"]); });

var area = d3.svg.area()

.interpolate("basis")

.x(function(d) { return x(d.date); })

.y1(function(d) { return y(d["income"]); });

});

如你所见,你实际上只定义了收入点的线;没有对费用值的引用。但是你对收支两条线之间的区域感兴趣,所以当你定义path元素时,为了画出这个区域,你可以把费用值作为一个边界,用一个通用函数迭代d值(见清单 20-66)。

清单 20-66。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

...

var area = d3.svg.area()

.interpolate("basis")

.x(function(d) { return x(d.date); })

.y1(function(d) { return y(d["income"]); });

svg.datum(data);

svg.append("path")

.attr("class", "area below")

.attr("d", area.y0(function(d) { return y(d.expense); }));

svg.append("path")

.attr("class", "line")

.attr("d", line);

});

如果你现在加载网页,你应该得到想要的区域(见图 20-23 )。

A978-1-4302-6290-9_20_Fig23_HTML.jpg

图 20-23。

An initial representation of the area between both trends

但是所有的区域都是绿色的。相反,你希望其中一些区域是红色的。您需要选择收入线和费用线所包围的区域,其中收入线在费用线之上,并排除与此方案不对应的区域。当您处理区域时,必须增加或减少部分区域,有必要引入裁剪路径 SVG 元素。

剪辑路径是 SVG 元素,可以用path元素附加到以前绘制的图形上。剪辑路径描述了一个“窗口”区域,它只显示在由路径定义的区域中。图形的其他区域保持隐藏。

看一下图 20-24 。你可以看到收入线又黑又粗。这条线以上的所有绿色区域(在印刷书籍版本中为浅灰色)应该被裁剪路径隐藏。但是你需要什么样的剪辑路径呢?您需要由界定收入线上方较低区域的路径描述的剪辑路径。

A978-1-4302-6290-9_20_Fig24_HTML.jpg

图 20-24。

Selection of the positive area with a clip path area

你需要对代码做一些修改,如清单 20-67 所示。

清单 20-67。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

...

svg.datum(data);

svg.append("clipPath")

.attr("id", "clip-below")

.append("path")

.attr("d", area.y0(h));

svg.append("path")

.attr("class", "area below")

.attr("clip-path", "url(#clip-below)")

.attr("d", area.y0(function(d) { return y(d.expense); }));

svg.append("path")

.attr("class", "line")

.attr("d", line);

});

现在你需要对红色区域做同样的事情(在印刷书籍版本中是深灰色)。总是从收入线和支出线之间的区域开始,您必须消除收入线以下的区域。所以,如图 20-25 所示,可以使用描述收入线以上区域的裁剪路径作为窗口区域。

A978-1-4302-6290-9_20_Fig25_HTML.jpg

图 20-25。

Selection of the negative area with a clip path area

将它转换成代码,你需要在代码中添加另一个clipPath,如清单 20-68 所示。

清单 20-68。ch20_09.html

d3.tsv("data_03.tsv", function(error, data) {

...

svg.append("path")

.attr("class", "area below")

.attr("clip-path", "url(#clip-below)")

.attr("d", area.y0(function(d) { return y(d.expense); }));

svg.append("clipPath")

.attr("id", "clip-above")

.append("path")

.attr("d", area.y0(0));

svg.append("path")

.attr("class", "area above")

.attr("clip-path", "url(#clip-above)")

.attr("d", area.y0(function(d) { return y(d.expense); }));

svg.append("path")

.attr("class", "line")

.attr("d", line);

});

最终,两个区域同时被绘制,你得到了想要的图表(见图 20-26 )。

A978-1-4302-6290-9_20_Fig26_HTML.jpg

图 20-26。

The final representation of the difference area chart

摘要

本章展示了如何构建折线图的基本元素,包括轴、轴标签、标题和网格。特别是,你已经了解了标度、定义域和范围的概念。

然后,您学习了如何从外部文件中读取数据,尤其是 CSV 和 TSV 文件。此外,在开始处理多系列数据时,您学习了如何实现多系列折线图,包括学习完成它们所需的所有元素,例如图例。

最后,您学习了如何创建一种特殊类型的折线图:差异折线图。这有助于您理解剪辑区域路径。

在下一章,你将处理条形图。利用到目前为止您所学到的关于 D3 的知识,您将会看到,仅仅使用 SVG 元素,就可以实现构建条形图所需的所有图形组件。更具体地说,您将看到如何使用相同的技术实现所有可能类型的多系列条形图,从堆叠条形图到分组条形图,包括水平和垂直方向的条形图。