JavaScript 图表入门指南(七)
十九、使用 D3
Abstract
这一章是本书第三部分的开始,关于 D3 库。这个库在书中有一个单独的章节专门介绍它,因为它在许多方面不同于 jqPlot 和 Highcharts 库。在本章的各个部分,以及在接下来的章节中,当你更深入地研究这个库的各个方面时,你将能够体会到 D3 有一个独特的和创新的结构。首先,它没有使用 jQuery,但是它再现了数据可视化所必需的所有特性。在 jqPlot 和 Highcharts 库中,已经创建了图表组件,只需要用户通过 options 对象来调整它们的属性,而 D3 实际上采用了相反的方法。
这一章是本书第三部分的开始,关于 D3 库。这个库在书中有一个单独的章节专门介绍它,因为它在许多方面不同于 jqPlot 和 Highcharts 库。在本章的各个部分,以及在接下来的章节中,当你更深入地研究这个库的各个方面时,你将能够体会到 D3 有一个独特的和创新的结构。首先,它没有使用 jQuery,但是它再现了数据可视化所必需的所有特性。在 jqPlot 和 Highcharts 库中,已经创建了图表组件,只需要用户通过 options 对象来调整它们的属性,而 D3 实际上采用了相反的方法。
D3 库允许您构建任何表示,从最基本的图形元素开始,比如圆、线、正方形等等。当然,这种方法会使图表的实现变得非常复杂,但同时,它允许您开发全新的图形表示,而不必遵循其他图形库提供的预设模式。
因此,在本章的过程中,你将熟悉这个库的基本概念。您将看到它们中的一些——比如选择、选择器和方法链——是如何从 jQuery 库中提取的。您还将了解如何操作各种文档对象模型(DOM)元素,尤其是可伸缩矢量图形(SVG)元素的创建,它们是图形表示的基本构件。
本章最后简要介绍了 SVG 元素的变换和转换。
你将从介绍这个奇妙的图书馆开始。
FIREBUG: DEBUGGING D3 CODE
在开始一些实际例子之前,我想提醒您使用 FireBug 进行调试。至少,一定要有一个好的 JavaScript 调试工具,允许你查看你将要处理的网页的 DOM 树(参见第一章中的“FireBug 和 DevTool”一节)。
使用 D3 库的调试工具是很重要的,因为与你见过的其他库不同,它不是由预先建模的对象构成的。使用 D3,需要从头开始,逐个实现所有的图表元素。因此,熟悉开发的人会意识到,选择一个好的调试工具对于解决出现的任何问题都是至关重要的。
使用 FireBug 可以编辑、调试和监控 CSS、SVG 和 HTML。您可以实时更改它们的值并查看效果。它还提供了一个控制台,您可以在其中读取日志,日志被适当地放置在 JavaScript 代码中,以监控所使用的变量的内容。这可以通过调用控制台对象的log()函数并将感兴趣的变量作为参数传递来实现:
console.log (variable);
也可以添加一些文本供参考:
console.log ("this is the value:");
您将看到,使用 D3 时,FireBug 对于检查 JavaScript 在 DOM 中生成的 SVG 元素的动态结构至关重要。
D3 简介
D3 是一个 JavaScript 库,类似于 jQuery 库,允许直接检查和操作 DOM,但仅用于数据可视化。它确实出色地完成了它的工作。实际上,D3 这个名字来源于数据驱动文档。D3 是由 Protovis 库的创建者 Mike Bostock 开发的,D3 的设计目的是取代它。
这个库被证明是非常通用和强大的,这要归功于它所基于的技术:JavaScript、SVG 和 CSS。D3 结合了强大的可视化组件和数据驱动的 DOM 操作方法。这样,D3 充分利用了现代浏览器的功能。
D3 允许您将任意数据绑定到 DOM。它的优势在于能够影响文档的几种转换。例如,一组数据可以转换成交互式 SVG 图形结构,比如图表。
您已经看到 jqPlot 作为一个 JavaScript 框架的优势在于它提供了结构化的解决方案,您可以通过选项的设置来操纵这些解决方案。与 jqPlot 不同,D3 的实力恰恰相反。它提供了构建块和工具来组装基于 SVG 的结构。这种方法的结果是新结构的不断发展,这是图形丰富,开放给各种各样的互动和动画。对于那些想要为现有框架没有涵盖的方面开发新的图形解决方案的人来说,D3 是一个完美的工具。
D3 不使用 jQuery 库,但是它有许多类似的概念,包括方法链范例和选择。它为 DOM 提供了一个类似 jQuery 的接口,这意味着您不需要非常详细地了解 SVG 的所有特性。为了处理 D3 代码,您需要能够使用对象和函数,并理解广泛使用的 SVG 和 CSS 的基础知识。为掌握所有这些知识所付出的牺牲会得到回报,你可以创造出令人惊叹的可视化效果。
SVG 为艺术作品提供了构建模块;它允许你画所有的基本形状,如直线、矩形、圆形和文本。它允许你用路径构建复杂的形状。
从一个空白的 HTML 页面开始
是时候实践刚刚概述的概念了。首先,从一个空白页开始,如清单 19-1 所示。这将是所有 D3 例子的起点。
清单 19-1。ch19_01a.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.js
<style>
// CSS Style here
</style>
</head>
<body>
<!-- HTML elements here -->
<script type="text/javascript">
// D3 code here
</script>
</body>
</html>
虽然乍一看,您只看到一个简单的 HTML 空白页面,但是在使用 D3 时,您必须采取一些小措施。最简单明了的方法是包含库 D3:
<script src="../src/d3.v3.js"></script>
或者,如果您喜欢使用内容交付网络(CDN)服务:
<script src="http://d3js.org/d3.v3.js
当输入远程 D3 库的 URL 时,确保网站总是包含最新版本。另一个不太明显的方法是添加页面的<head>:
<meta charset="utf-8">
如果不指定这一行,您很快就会发现您添加的 D3 代码无法运行。最后,但同样重要的是,在哪里添加代码的各个部分非常重要。建议将 D3 的所有 JavaScript 代码放在<body>部分的末尾,在所有 HTML 元素之后。
使用选择和运算符
要开始使用 D3,有必要熟悉选择的概念。处理选择涉及到三个基本对象的使用:
- 选择
- 选择器
- 经营者
选择是从当前文档中提取的节点元素的数组。为了提取一组特定的元素(选择),您需要使用选择器。这些模式匹配文档树结构中的元素。一旦你得到一个选择,你可能希望对它执行一些操作,所以你使用操作符。作为它们操作的结果,您得到一个新的选择,因此可以应用另一个操作符,以此类推。
选择器和操作符是由 W3C(万维网联盟)API 定义的,所有现代浏览器都支持。通常,您将操作 HTML 文档,因此您将选择 HTML 元素。
选择和选择器
为了从文档中提取选择,D3 提供了两种方法:
selectselectAll
选择与选择器匹配的第一个元素,返回只有一个元素的选择。
相反,选择所有与选择器匹配的元素,返回包含所有这些元素的选择。
要理解这些概念,最好的方法就是通过一些简单的例子来逐步理解。从刚才描述的 HTML 页面开始,添加包含一些文本的两段,然后用 D3 进行选择(见清单 19-2)。
清单 19-2。ch19_01a.html
<body>
<p>First paragraph</p>
<p>Second paragraph</p>
<script type="text/javascript">
var selection = d3.select("p");
console.log(selection);
</script>
</body>
d3.select是顶级操作员;"p"是选择器;而selection是你赋给变量的运算符的返回值。使用这个 D3 命令,您想要选择 web 页面中的第一个元素<p>。使用log功能,你可以在图 19-1 中看到 FireBug 的选择。
图 19-1。
The FireBug console enables you to see the content of the selection
因为您使用了select()方法,所以您的选择只有一个元素,尽管在 web 页面中有两个元素。如果你想两者都选,你可以使用selectAll(),如清单 19-3 所示。
清单 19-3。ch19_01b.html
<script type="text/javascript">
var selection = d3.selectAll("p");
console.log(selection);
</script>
图 19-2 显示了这两种元素。
图 19-2。
FireBug shows the selection of all the
elements in the web page
现在您有一个包含两个元素的选择。jQuery 和 D3 引入选择概念最大创新是不再需要循环。您可以一次对整个选择进行操作,而不是编写递归函数来修改元素。
经营者
一旦你学会了选择,是时候对它们应用操作符了。
操作符是一种应用于选择或一组元素的方法,它专门“操作”一个操作。例如,它可以获取或设置选择中元素的属性,或者以某种方式对其内容进行操作。例如,您可能想要用新文本替换现有文本。为此,您使用了text()操作符,如清单 19-4 所示。
清单 19-4。ch19_02.html
<body>
<p>First paragraph</p>
<p>Second paragraph</p>
<script type="text/javascript">
var selection = d3.selectAll("p");
selection.text("we add this new text");
</script>
</body>
该页面现在为同一文本报告两次,而之前有两个段落(见图 19-3 )。
图 19-3。
The text contained in the two <p> elements has been replaced in the browser on the left and is shown in FireBug on the right
您定义了变量选择,然后将运算符应用于该变量。但是还有另一种方式来写这一切;您可以使用链功能的方法,尤其是当您将多个运算符应用于同一个选择时。
d3.selectAll("p").text("we add this new text");
您已经看到,通过向text()操作符传递一个参数,您将替换现有的文本。所以就好像函数是setText("new text")。但是你并不总是想要那样。如果不传递任何参数,该函数将有不同的行为。它将返回已经存在的文本的值。这对于进一步的处理,或者将该字符串值赋给变量或数组非常有用。因此,如果没有参数,它就好像是getText()。
var text = d3.select("p").text();
console.log(text);
文本变量包含"First paragraph"字符串(见图 19-4 )。
图 19-4。
The FireBug console shows the text contained in the selection
你想要操作的每一种对象都有操作符。这些操作员可以设置以下内容:
- 属性
- 风格
- 性能
- 超文本标记语言
- 文本
您刚刚看到了text()操作符的运行。接下来,您将看到其他一些操作符。
Note
如果你想了解更多关于操作符的知识,我建议你访问 D3 库的 API 参考,链接: https://github.com/mbostock/d3/wiki/API-Reference 。
例如,能够改变 CSS 样式是很有帮助的,你可以用style()操作符来实现。清单 19-5 使用text()替换了现有的文本,然后修改其样式为红色,在方法链中添加了style()操作符。
清单 19-5。ch19_03.html
<body>
<p>Existing black text</p>
<script type="text/javascript">
d3.selectAll("p").style('color','red').text("New red text");
</script>
</body>
图 19-5 左侧显示原始文本,右侧显示新样式的文本。
图 19-5。
The original text is replaced by the new red text, applying the chain method upon the selection
另一个操作符attr(),作用于元素的属性层。创建新的 SVG 元素时将使用此运算符;事实上,它允许您在创建标签时定义属性,然后再将它们插入到网页中。在这里,您可以看到它如何修改现有的属性。在清单 19-6 中,你正在改变显示在页面中间的标题的对齐方式(见图 19-6 )。
清单 19-6。ch19_04.html
<body>
<h1>Title</h1>
<script type="text/javascript">
d3.select('h1').attr('align','center');
</script>
</body>
图 19-6。
With the D3 library it is possible to dynamically add a title to a web page
创建新元素
既然您已经看到了如何在元素级别上操作以及如何修改属性和内容,那么是时候看看如何创建新的项目了。为此,D3 提供了许多运算符( https://github.com/mbostock/d3/wiki/API-Reference ,其中最常用的有:
html()append()insert()
html()方法
本节展示了html()方法是如何操作的。您总是从一个选择开始,然后应用这个操作符在里面添加一个元素。例如,您选择一个特定的标签作为容器,然后编写一个字符串作为参数传递。然后这个字符串成为标签的内容(见清单 19-7)。
清单 19-7。ch19_05.html
<body>
<p>A paragraph</p>
<script type="text/javascript">
d3.select('p').html("<h1>New Paragraph</h1>");
</script>
</body>
这里,首先用select()选择<p>标签,然后用html()用新元素<h1>替换它的内容。图 19-7 左侧显示原始文本,右侧显示新格式化的版本。
图 19-7。
The text in a paragraph element <p> is replaced with a heading element <h>
使用 FireBug 可以更好地看到这种变化(参见图 19-8
图 19-8。
FireBug clearly shows the insertion of the head element (on the right) to replace the content of the paragraph element (on the left)
实际上,html()函数用作为参数传递的 HTML 代码替换选择的内容。顾名思义,这个函数允许您在选择的元素中动态编写 HTML 代码。
append()方法
另一种流行的添加元素的方法是append()。
回想一下,当您使用html()操作符时,所选标记的内容(如果有的话)将被替换为作为参数传递的新内容。append()操作符改为添加一个新元素,作为它的参数传递到所选标签中包含的所有现有元素的末尾。新创建的元素的内容必须添加到方法链中,如果它只是一个字符串,则使用text(),如果它是另一个元素,则使用append()、html()或insert()。
为了理解这最后一点,在页面上添加一个无序列表<ul>,其中包含一些包含水果名称的条目(见图 19-9 )。
图 19-9。
An unordered list of three fruits
假设您现在想将橙子添加到这个列表中。为此,您必须选择无序列表标签<ul>,然后使用append()添加一个列表项标签<li>。但是append()只创建了标签,所以为了将字符串"Oranges"插入其中,你需要将text()操作符添加到方法链中(见清单 19-8)。
清单 19-8。ch19_06.html
<body>
<ul>
<li>Apples</li>
<li>Pears</li>
<li>Bananas</li>
</ul>
<script type="text/javascript">
d3.select('ul').append('li').text("Oranges");
</script>
</body>
图 19-10 显示了添加了元素的列表。
图 19-10。
Using the append() operator, you have added the Oranges item to the end of the list
图 19-11 在 FireBug 中显示。
图 19-11。
FireBug shows the HTML structure with the added <li> element
在这种情况下,您已经使用简单的文本作为添加到列表中的新元素的内容,但是append()操作符可以做更多的事情。事实上,如前所述,一个元素的内容可以是另一个元素。这允许你创建一个完整的 HTML 元素树,所有这些都是通过一个链式方法实现的。事实上,由append()操作符创建的新元素的内容可以由另一个操作符创建,比如另一个append()操作符。请看清单 19-9。这是一个简单的例子,可以帮助你更好地理解这个概念。
这一次,您想要创建一个水果的子类——柑橘类水果,在这个子类中,我们将分配橙子、柠檬和葡萄柚项目。为此,您需要添加一个新的列表项<li>,以字符串"Citrus fruits"作为其内容。这与上一个例子的工作方式相同,在append()操作符之后连接text()操作符。然后,您需要创建一个新的列表项。这一次,它的内容是一个无序列表。因此,您需要连接两个append()操作符,以便创建一个嵌套在无序列表<ul>元素中的列表项<li>元素。然后,您可以向嵌套的无序列表中添加另外两个新元素,同样使用append()操作符。
清单 19-9。ch19_06b.html
<body>
<ul>
<li>Apples</li>
<li>Pears</li>
<li>Bananas</li>
</ul>
<script type="text/javascript">
d3.select('ul').append('li').text("Citrus fruits");
d3.select('ul').append('ul').append('li').text("Oranges");
d3.select('ul').select('ul').append('li').text("Lemons");
d3.select('ul').select('ul').append('li').text("Grapefruits");
</script>
</body>
图 19-12 显示了浏览器上新的柑橘类水果嵌套列表以及在 FireBug 上生成它的 HTML 结构。
图 19-12。
FireBug shows the HTML structure with a nested unordered list in the browser on the left and in FireBug on the right
insert()方法
最后一个操作符insert()有一个特殊的行为。如果只使用一个参数,它的行为就像使用append()一样。通常,它与两个参数一起使用。第一个指示要添加的标记,第二个是要插入的匹配标记。实际上,将前面水果清单示例中的append()替换为insert(),会得到不同的结果,如图 19-13 (左边是原来的清单,右边是添加了橙子的新清单)。
d3.select('ul').insert('li','li').text("Oranges");
图 19-13。
Using the insert() operator, you can insert the Oranges item at the top of the list
现在新元素位于无序列表的顶部。但是如果您想在不同于第一个项目的位置插入一个新项目呢?您可以使用 CSS 选择器nth-child(i)来做到这一点,其中i是元素的索引。因此,如果您使用选择器li:nth-child(i),您将选择第 I 个<li>元素。因此,如果你想在第二个和第三个元素之间插入一个元素,你需要在insert()操作符中调用第三个<li>元素(记住这个操作符把新元素放在被调用的元素之前):
d3.select('ul').insert('li','li:nth-child(2)').text("Oranges");
这将在列表的第二个和第三个项目之间插入新的橙色项目,如图 19-14 所示(在左边的浏览器和右边的 FireBug 中)。
图 19-14。
Using the CSS selector nth-child, you can add the Oranges item in any position in the list HTML(), APPEND(), AND INSERT() OPERATORS: A BETTER UNDERSTANDING
有时,理解这三个操作符的功能并不容易。考虑这个示意性的 HTML 结构,其中包含一个通用的父标签和一些子标签:
<parent>
<child></child>
<child></child>
<child></child>
</parent>
为了更好地理解不同的行为,下面的简单图表显示了每个操作符具体做什么。如果您想充分利用 D3 库的潜力,完全理解这三个操作符的功能是至关重要的。
当您需要在 HTML 结构的同一层的其他标签列表的末尾创建一个新的标签元素时,使用append()操作符。图 19-15 显示了该操作员的行为。
图 19-15。
The append() operator adds a child tag to the end of the list
当您需要在 HTML 结构的同一层的其他标签列表的开头创建一个新的标签元素时,使用insert()操作符。图 19-16 显示了该操作员的行为。
图 19-16。
The insert() operator adds a child tag before the child tag is passed as a second argument
当您需要在其他标签列表中的特定位置创建一个新的标签元素时,总是在 HTML 结构的同一层,使用insert()操作符。图 19-17 显示了该操作员的行为。
图 19-17。
You can pass a child of the list as the argument using the CSS selector nth-child()
当你需要创建一个新的标签元素来代替另一个标签或者 HTML 结构中同一层的一系列标签时,使用html()操作符。图 19-18 说明了该操作员的行为。
图 19-18。
The html() operator replaces the contents of the parent tag with the tag passed as the argument
将数据插入元素
您已经看到了如何在文档中创建新的元素。但是你怎么能把数据放进去呢?这就是data()操作符的用武之地,它将一组数据作为参数传递。
对于选择中的每个元素,将按照序列的相同顺序在数组中赋值。这种对应关系由一个通用函数表示,其中d和i作为参数传递。
function(d,i) {
// code with d and i
// return some elaboration of d;
}
只要列表中有元素,这个函数就会执行很多次:i是序列的索引,d是对应于该索引的数据数组中的值。很多时候你对i的值不感兴趣,只用d。
对于那些熟悉for循环的人来说,就好像你写了:
for(i=0; i < selection.length; i++){
d = input_array[i];
// code with d and i
//return output_array[i];
}
要了解整个事情,没有比提供一个例子更好的方法了。定义一个包含三种水果名称的数组。您将创建一个包含三个空条目的无序列表,并用selectAll()创建这些条目的选择。选择中必须有相应数量的项目,数组中必须有相应数量的值;否则,将不评估剩余价值。您将数组与选择相关联,然后应用function(d),在列表中写入每一项的值(参见清单 19-10)。
清单 19-10。ch19_07.html
<body>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
<script type="text/javascript">
var fruits = ['Apples', 'Pears', 'Bananas'];
d3.selectAll('li').data(fruits).text( function(d){
return d;
});
</script>
</body>
图 19-19 显示了左侧浏览器和右侧 FireBug 中的结果。在 FireBug 中,您可以看到用于每个列表项<li>内容的 HTML 结构,这在您编写清单 19-10 时是不存在的。这些添加的文本项是fruits数组的值。
图 19-19。
It is possible to fill the content of HTML elements with array values
data()操作符不仅仅将数据绑定到元素,它还计算选择的元素和提供的数据之间的联系。只要数组的长度等于所选元素的数量,一切都会很顺利。但如果不是这样呢?如果您有一个包含三个元素的选择,并提供一个包含五个值的数组,那么这两个额外的值将存储在一个名为“enter”的特殊选择中这个选项可以通过数据调用返回值上的enter()操作符来访问。你可以在清单 19-11 的例子中看到这一点。
清单 19-11。ch19_08.html
<body>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
<script type="text/javascript">
var fruits = ['Apples', 'Pears', 'Bananas', 'Oranges', 'Strawberries'];
var list = d3.select('ul');
var fruits = list.selectAll('li').data(fruits);
fruits.enter().append('li').text( function(d){
return d;
});
fruits.text( function(d){
return d;
});
</script>
</body>
首先,用五种不同的水果定义数组。然后,选择包含列表的内容,并将其分配给变量列表。从这个选择中,您可以进一步选择包含三个空列表项的列表项,并将fruits数组分配给它。从这个关联中,数组的最后两个值将前进(橙子和草莓),因此它们将被存储在enter选择中。现在你必须特别注意这一点:通常最好先处理enter选择。因此,您必须访问enter选项并使用append()来创建两个新的列表项,其中包含两个高级水果。然后,在三个现有列表项的水果选择中写入值。
您将得到一个包含所有五种水果的列表,按照输入的顺序排列。图 19-20 顶部显示了浏览器的变化,底部显示了 FireBug 的变化。
图 19-20。
It is possible to fill the content of HTML elements with array values and to integrate them with other elements if they are not enough
应用动态属性
您已经看到了如何使用 D3 框架提供的函数来定义和修改样式、属性和其他属性。但到目前为止,它们都被当作常数。是时候向前迈出一大步了。JavaScript 语言的优势之一,尤其是 D3(和 jQuery)库,在于它能够使页面内容动态化。事实上,您已经看到了如何在 web 页面中删除、创建和操作元素标签。类似的方法也适用于其他类型的值,如 CSS 样式或通过选择机制创建或操作的元素的属性。您甚至可以创建与事件或控件相关的不同选项。
D3 为此提供了一组特定的函数。尽管这些函数看起来很简单,但对于那些知道如何充分利用其机制的人来说,它们可能是一个强大的工具。
在清单 19-12 的例子中,你使用一个普通的函数给段落分配一个随机的颜色。每次加载页面时,它都会显示一组不同的颜色。
清单 19-12。ch19_09.html
<body>
<p>the first paragraph</p>
<p>the second paragraph</p>
<p>the third paragraph</p>
<p>the last paragraph</p>
<script>
d3.selectAll("p").style("color", function() {
r = Math.round((Math.random() * 255));
g = Math.round((Math.random() * 255));
b = Math.round((Math.random() * 255));
return "rgb("+r+", "+g+", "+b+")";
});
</script>
</body>
左边的图 19-21 显示了一个加载页面和另一个页面的结果,右边的页面应用了不同的颜色。每次加载页面,都会得到不同的颜色组合。
图 19-21。
The colors change each time the page loads
当然,这是一个非常简单的例子,但是它展示了基本的思想。分配给属性、文本或样式的任何值都可以通过函数动态生成。
添加 SVG 元素
你终于可以运用你所学的知识来创造漂亮的展示了。在本节中,您将开始学习 D3 库的特性,创建和操作图形元素,如直线、正方形、圆形等等。所有这些将主要通过使用两个标签的嵌套结构来完成:<svg>用于图形元素,<g>用于应用组。
首先,您将学习如何创建一个 SVG 元素,以及如何使用<g>标签将它嵌套在一个组中。稍后,您将发现什么是 SVG 转换,以及如何将它们应用于元素组。最后,通过另一个例子,您将看到如何用 SVG 转换来制作这些元素的动画,以获得漂亮的动画。
创建 SVG 元素
您可以从一个<div>标签开始,它将被用作可视化的容器,类似于 jQuery 对<canvas>所做的。从这个<div>标签,您使用append()操作符创建根标签<svg>。然后你可以通过使用attr()操作符作用于height和width属性来设置可视化的大小(见清单 19-13)。
清单 19-13。ch19_10.html
<body>
<div id="circle"></div>
<script type="text/javascript">
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
</script>
</body>
从 FireBug 中,你可以看到带有新的<svg>元素的<body>结构及其属性(见图 19-22 )。
图 19-22。
FireBug shows the <svg> tag you just created
您还可以向根标签<svg>添加一个基本形状。让我们添加一个黄色的圆圈(见清单 19-14)。一旦你理解了这个原则,无论何时你想重复它都是非常简单的。
清单 19-14。ch19_10.html
<script type="text/javascript">
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('circle')
.style('stroke', 'black')
.style('fill', 'yellow')
.attr('r', 40)
.attr('cx', 50)
.attr('cy', 50);
</script>
图 19-23 显示了完美的黄色圆圈。
图 19-23。
A perfect yellow circle
在 FireBug 中,你可以看到标签的树形结构是如何从根<svg>开始逐渐形成的,指定了所有的属性(见图 19-24 )。
图 19-24。
In FireBug, it is possible to follow the development of the tag structure
现在您已经看到了如何使用 SVG 标签创建图形,下一步是对它们应用转换。
转换
D3 的一个关键方面是它的转换能力。这扩展了 JavaScript 中 SVG 转换的概念。一旦在 SVG 中创建了一个对象,从简单的正方形到更复杂的结构,它都可以进行各种变换。最常见的转换包括:
- 规模
- 翻译
- 辐状的
Note
如果你有兴趣了解更多关于转换的知识,我建议你访问这个页面: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 。它列出了所有可用的转换,并附有简单的解释。
通常,您使用这些基本转换的序列来获得更复杂的转换。和往常一样,您将看到一系列小例子来说明转换的概念。首先,你将画一个红色的小方块,就像你画黄色圆圈一样(见清单 19-15)。为此,您可以使用<rect>标签。与<circle>唯一不同的是,对于矩形,你需要用 x 和 y 来指定矩形左上角的位置,而不是圆心。然后你要指定矩形的大小,既然是正方形,那么边就要相等。
清单 19-15。ch19_11a.html
<div id="square"></div>
<script type="text/javascript">
var svg = d3.select(``'#square'
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('rect')
.style('stroke', 'black')
.style('fill', 'red')
.attr('x', 50)
.attr('y', 50)
.attr('width', 50)
.attr('height', 50);
</script>
现在是引入另一个概念的好时机,这个概念在处理 SVG 元素时会很有用:元素组。您通常需要对一组形状或一个复杂形状(由多个基本形状组成)应用一系列操作,有时仅包括变换。这可以通过将几个项目组合在一个组中来实现,这可以通过将所有元素放在一个标签<g>中在 SVG 中反映出来。例如,如果你想对红色方块应用一个变换,你需要把它插入一个组中(见清单 19-16)。
清单 19-16。ch19_11a.html
var svg = d3.select('#square')
.append('svg')
.attr('width', 200)
.attr('height', 200);
var g = svg.append("svg:g");
g.append('rect')
.style('stroke', 'black')
.style('fill', 'red')
.attr('x', 50)
.attr('y', 50)
.attr('width', 50)
.attr('height', 50);
图 19-25 显示了 SVG 结构在 FireBug 中的表现。
图 19-25。
FireBug shows the SVG structure corresponding to the red square
在浏览器中,你会看到一个如图 19-26 所示的红色小方块。
图 19-26。
A red square is a good object upon which to apply transformations
现在,您将一个接一个地应用所有的变换。从平移开始,在 SVG 中由translate (x, y)函数表示,其中x和y是方块移动的像素数量(见清单 19-17)。
清单 19-17。ch19_11b.html
var g = svg.append("svg:g")
.attr("transform", "translate(" + 100 + ",0)");
在这里,我将值100放在作为属性传递的字符串之外,以便理解在这一点上您可以插入一个先前定义的变量。这将使转变更有活力。用这条线,你将正方形向右移动了 100 个像素(见图 19-27 )。
图 19-27。
Now the red square appears right-shifted by 100 pixels
另一种可以应用于正方形的变换叫做缩放。在这个 SVG 中,通过函数scale(s)或scale(sx, sy)来表示。如果在函数中传递单个参数,缩放将是一致的,但如果传递两个参数,您可以在水平和垂直方向上以不同的方式应用正方形的扩展。清单 19-18 将红色方块的大小增加了两倍。因此,您需要应用scale()转换,并将值2作为参数传递。传递的数字是正方形大小要乘以的因子。因为您已经传递了一个参数,所以缩放是一致的。
清单 19-18。ch19_11c.html
var g = svg.append("svg:g")
.attr("transform","scale(2)");
图 19-28 显示了缩放了两倍的正方形。这个广场的高度和宽度都增加了一倍。
图 19-28。
The red square has doubled its size
如果你想要非均匀缩放,你可以使用类似清单 19-19 的东西来获得类似图 19-29 的结果。不均匀的缩放会扭曲一个图形,从而产生另一个图形。在这种情况下,你从一个正方形得到一个矩形。
清单 19-19。ch19_11d.html
var g = svg.append("svg:g")
.attr("transform","scale(2, 1)");
图 19-29。
A rectangle obtained by applying non-uniform scaling to a square
最后一种变换是旋转。用函数rotate(degree,x,y)在 SVG 中表示,其中第一个自变量是以度为单位的旋转(顺时针)角度,x和y是旋转中心的坐标。
假设您希望旋转的中心与正方形的中心相对应,正方形位于 x = 75 和 y = 75 处。如果你想画一个菱形,你需要在正方形上旋转 45 度(见清单 19-20)。
清单 19-20。ch19_11e.html
var g = svg.append("svg:g")
.attr("transform","rotate(45, 75, 75)");
你得到菱形(见图 19-30 )。
图 19-30。
A rhombus is the result you obtain when you rotate a square
但是最有趣的效果涉及到在一个序列中应用转换,从而创建一个链(见清单 19-21)。
清单 19-21。ch19_11f.html
var g = svg.append("svg:g")
. attr("transform", "translate(-30, 0),scale(2, 1),rotate(45, 75, 75)");
从这个列表中,你得到了图 19-31 中的形状。
图 19-31。
A rhombus obtained by applying a chain of transformations to a square
过渡
您已经看到,属性、样式等的值可以是动态的,这取决于借助某些函数设置的定义。但是 D3 提供了更多——你甚至可以动画你的形状。D3 为此提供了三个功能:
transition()delay()duration()
很自然,您会将这些函数应用于 SVG 元素,这要感谢 D3,它可以识别任何类型的值并对它们进行插值。
当 SVG 形状从一种状态转换到另一种状态时,您可以定义转换。起始状态和最终状态都由几个参数来表征,这些参数定义了对象的颜色、形状、大小和位置。你把黄圈例子中定义的状态作为初始状态(参见清单 19-14)。在清单 19-22 中,你让圆经历一个由三种不同变化组成的过渡:圆把它的颜色变成黑色(把fill设置成black),它缩小它的面积(把r从40变成10),它稍微向右移动(把cx从50变成150)。
清单 19-22。ch19_12.html
<div id="circle"></div>
<script>
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('circle')
.style('stroke', 'black')
.style('fill', 'yellow')
.attr('r', 40)
.attr('cx', 50)
.attr('cy', 50)
.transition()
.delay(100)
.duration(4000)
.attr("r", 10)
.attr("cx", 150)
.style("fill", "black");
</script>
因此,在这个例子中,您将transition()方法添加到方法链中。这将初始状态与最终状态分开,并警告 D3 发生了转换。紧随transition()之后的是另外两个功能:delay()和duration()。
delay()函数有一个参数:转换开始前必须经过的时间。相反,duration()函数被定义为过渡所用的时间。传递的参数值越大,过渡越慢。
在这三个函数之后,您将所有表征图形最终状态的属性添加到方法链中。D3 根据你建立的时间来插入中间值,并且会用这些值生成所有的中间数字。出现在你眼前的是一个动画,黄色的圆圈变成黑色,向左移动,尺寸减小。所有这一切只需要四秒钟。
图 19-32 显示了过渡序列,由此可以看到圆的变化。
图 19-32。
Different instances of the animation of a circle subjected to transitions
到目前为止,您看到的简单示例一次应用于一个图形元素。下一步是将你所学到的应用到元素组中,从而创建更复杂的图形。后续章节提供了将 D3 库的基本概念付诸实践的好例子。
摘要
本章介绍了 D3 库的亮点。即使不使用 jQuery 库,D3 也能以非常相似的方式管理选择、选择器和操作符。通过一系列示例,您已经看到了如何通过更改 DOM 元素的属性以及在需要时创建新的属性来操作 DOM 元素。在本章的第二部分,你学习了 D3 库操作的主要对象是什么:SVG 元素。这些是构建图表的图形构件。最后,您快速查看了如何将 SVG 转换应用于这些图形元素,然后查看了如何利用 SVG 转换来生成漂亮的动画。
下一章通过实现折线图,将你到目前为止学到的关于 D3 库的知识付诸实践。通过一个接一个的 SVG 元素,您将看到如何获得与 jqPlot 和 Highcharts 库类似的结果。
二十、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.js
下一个“砖块”由清单 20-1 中的输入数据数组组成。该数组包含数据系列的 y 值。
清单 20-1。ch20_01.html
var data = [100, 110, 140, 130, 80, 75, 120, 130, 100];
清单 20-2 定义了一组与绘制图表的可视化尺寸相关的变量。w和h变量是图表的宽度和高度;页边距用于在图表边缘留出空间。
清单 20-2。ch20_01.html
w = 400;
h = 300;
margin_x = 32;
margin_y = 20;
因为您在基于 x 轴和 y 轴的图形上工作,所以在 D3 中有必要为这两个轴中的每一个定义一个标度、一个域和一个值范围。我们先来理清这些概念,了解一下它们在 D3 中是如何管理的。
比例、域和范围
你已经不得不处理规模,即使你可能没有意识到这一点。线性标度更容易理解,尽管在一些例子中,你已经使用了对数标度(参见第九章中的侧栏“对数标度”)。标度只是将某个区间(称为域)中的值转换为属于另一个区间(称为范围)的另一个值的函数。但是这一切到底意味着什么呢?这对你有什么帮助?
实际上,每当您想要影响属于不同区间的两个变量之间的值的转换,但保持其相对于当前区间的“意义”时,这都可以为您服务。这涉及到规范化的概念。
假设您想要转换来自仪器的值,例如万用表报告的电压。您知道电压值应该在 0 到 5 伏之间,这是值的范围,也称为域。
您想要转换在红色标尺上测得的电压。使用红绿蓝(RGB)代码,该值将介于 0 和 255 之间。您现在已经定义了另一个颜色范围,即范围。
现在假设万用表上的电压读数是 2.7 伏,红色显示的色标对应的是 138(实际是 137.7)。您刚刚对值的转换应用了线性标度。图 20-1 显示了电压值到 RGB 刻度上相应 R 值的转换。这种转换在线性范围内进行,因为值是线性转换的。
图 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 中画布的功能。因此,你需要用w和h来指定画布的大小。在<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 里,这样的扫描通过使用参数d和i被不同地处理。数组当前项的索引用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 所示的图像。
图 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 所示的一行。
图 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。它的可能值是middle、start和end,这取决于您希望标签分别居中对齐、向左对齐还是向右对齐。
这里,您使用 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 为该阶段的折线图。
图 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 所示。
图 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 显示了以正确顺序绘制的元素的折线图。事实上,代表输入数据的蓝线现在位于覆盖网格的前景上,而不是相反。
图 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.max和d3.min函数,但是这些函数只对数组起作用,对对象不起作用。您插入的输入数据数组是一个对象数组。这个怎么解决?有几种方法。也许最直接的方法是扫描数据,找出 x 和 y 的最大值。在清单 20-16 中,你定义了两个包含这两个最大值的变量。然后一次扫描每个物体的 x 和 y 的值,你将 x 和 y 的当前值与xMax和yMax的值进行比较,看哪个值更大。两者中较大的将成为新的最大值。
清单 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,``yMax
x = d3.scale.linear().domain([0,``xMax
...
// 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 值所做的更改的结果。
图 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, yUpLim
x = d3.scale.linear().domain([``xLowLim, xUpLim
...
//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°之间的新折线图,更好地显示了线条。
图 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 显示了填充和未填充的结果。
图 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 显示了添加到折线图顶部的标题。
图 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 显示了放在相应轴旁边的两个新轴标签。
图 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.js
<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);
这使您可以专注于数据,而所有与轴相关的问题(记号、标签等)都由轴组件自动处理。因此,在您创建了xAxis和yAxis之后,您将 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 显示了结果。
图 20-12。
An empty chart ready to be filled with data
使用 FireBug,您可以看到刚刚定义的 SVG 元素的结构(参见图 20-13 )。
图 20-13。
FireBug shows the structure of the SVG elements created dynamically to display the axes
您可以看到所有的元素都被自动分组到组<g>标签中。这使您能够更好地将可能的转换应用到单独的元素。
如果需要,您也可以添加网格。你建立网格的方法和建立轴的方法一样。事实上,以同样的方式,您使用清单 20-33 中的axis()函数定义了两个网格变量——xGrid和yGrid。
清单 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 元素的水平网格线。
图 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>组的好例子。以前,组和其中的所有元素都是由函数创建的;现在你需要明确地做到这一点。如果你想给一个组添加两个轴标签,给另一个组添加标题,你需要指定两个不同的变量:labels和title(见清单 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 文件中所有数据的漂亮折线图。
图 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 库得到的结果非常相似。
图 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 显示了相同的折线图,但这次它使用红色小方块作为标记。
图 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 显示了黄色菱形的标记。
图 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")
...
如您所见,要定义一个区域,您需要指定三个界定边缘的函数:x、y0和y1。在这种情况下,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 显示了从折线图派生的面积图。
图 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.js
<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.min和d3.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 所示。
图 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 所示,带有图例。
图 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 显示了应用于图表中三个系列的插值线。连接数据点的直线已被曲线取代。
图 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.js
<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.max和Math.min比较每一步的收入和费用值,然后用d3.min和d3.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 )。
图 20-23。
An initial representation of the area between both trends
但是所有的区域都是绿色的。相反,你希望其中一些区域是红色的。您需要选择收入线和费用线所包围的区域,其中收入线在费用线之上,并排除与此方案不对应的区域。当您处理区域时,必须增加或减少部分区域,有必要引入裁剪路径 SVG 元素。
剪辑路径是 SVG 元素,可以用path元素附加到以前绘制的图形上。剪辑路径描述了一个“窗口”区域,它只显示在由路径定义的区域中。图形的其他区域保持隐藏。
看一下图 20-24 。你可以看到收入线又黑又粗。这条线以上的所有绿色区域(在印刷书籍版本中为浅灰色)应该被裁剪路径隐藏。但是你需要什么样的剪辑路径呢?您需要由界定收入线上方较低区域的路径描述的剪辑路径。
图 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 所示,可以使用描述收入线以上区域的裁剪路径作为窗口区域。
图 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 )。
图 20-26。
The final representation of the difference area chart
摘要
本章展示了如何构建折线图的基本元素,包括轴、轴标签、标题和网格。特别是,你已经了解了标度、定义域和范围的概念。
然后,您学习了如何从外部文件中读取数据,尤其是 CSV 和 TSV 文件。此外,在开始处理多系列数据时,您学习了如何实现多系列折线图,包括学习完成它们所需的所有元素,例如图例。
最后,您学习了如何创建一种特殊类型的折线图:差异折线图。这有助于您理解剪辑区域路径。
在下一章,你将处理条形图。利用到目前为止您所学到的关于 D3 的知识,您将会看到,仅仅使用 SVG 元素,就可以实现构建条形图所需的所有图形组件。更具体地说,您将看到如何使用相同的技术实现所有可能类型的多系列条形图,从堆叠条形图到分组条形图,包括水平和垂直方向的条形图。