JavaScript-图表入门指南-九-

104 阅读24分钟

JavaScript 图表入门指南(九)

原文:Beginning JavaScript Charts

协议:CC BY-NC-SA 4.0

二十三、D3 蜡烛图

Abstract

在这简短但重要的一章中,你会看到蜡烛图。这种类型的图表是基于一种特殊的数据格式(OHLC,或开盘-盘高-盘低-收盘),你已经在介绍 jqPlot 库时处理过了(见第十二章)。使用 jqPlot,您有一个特殊的插件来以适当的方式处理和表示这样的数据;相反,用 D3 你必须一个接一个地构建所有的图形元素,最重要的是你需要实现一个解析器从外部文件读取 OHLC 数据。此外,您需要解决的另一个重要方面是如何处理日期和时间数据。

在这简短但重要的一章中,你会看到蜡烛图。这种类型的图表是基于一种特殊的数据格式(OHLC,或开盘-盘高-盘低-收盘),你已经在 jqPlot 库介绍时处理过了(见第十二章)。使用 jqPlot,您有一个特殊的插件来以适当的方式处理和表示这样的数据;相反,用 D3 你必须一个接一个地构建所有的图形元素,最重要的是你需要实现一个解析器从外部文件读取 OHLC 数据。此外,您需要解决的另一个重要方面是如何处理日期和时间数据。

虽然这听起来很复杂,但在这一章中,你会发现 D3 库是如何为你提供工具,让事情变得简单而直接。

您将首先从构建一个简单的 OHLC 图表开始,以便特别关注 OHLC 数据的读取。然后您将详细了解 D3 如何处理日期和时间数据,最后您将仅使用标量矢量图形(SVG)元素(如线条)来表示 OHLC 图表。

在最后一部分,你将通过一些修改把你的 OHLC 图表转换成一个更完整的烛台图表。

创建 OHLC 图表

因为 D3 能够从小的图形组件构建新的图形结构,所以您还可以创建像用 jqPlot 生成的那些蜡烛图。您已经看到蜡烛图需要定义良好的数据结构:由日期和四个 OHLC 值组成的数据时间轴。您将清单 23-1 中的数据复制到一个文件中,并保存为data_08.csv

清单 23-1。data_08.csv

date,open,min,max,close,

08/08/2012,1.238485,1.2327,1.240245,1.2372,

08/09/2012,1.23721,1.22671,1.23873,1.229295,

08/10/2012,1.2293,1.22417,1.23168,1.228975,

08/12/2012,1.229075,1.22747,1.22921,1.22747,

08/13/2012,1.227505,1.22608,1.23737,1.23262,

08/14/2012,1.23262,1.23167,1.238555,1.232385,

08/15/2012,1.232385,1.22641,1.234355,1.228865,

08/16/2012,1.22887,1.225625,1.237305,1.23573,

08/17/2012,1.23574,1.22891,1.23824,1.2333,

08/19/2012,1.23522,1.23291,1.235275,1.23323,

08/20/2012,1.233215,1.22954,1.236885,1.2351,

08/21/2012,1.23513,1.23465,1.248785,1.247655,

08/22/2012,1.247655,1.24315,1.254415,1.25338,

08/23/2012,1.25339,1.252465,1.258965,1.255995,

08/24/2012,1.255995,1.248175,1.256665,1.2512,

08/26/2012,1.25133,1.25042,1.252415,1.25054,

08/27/2012,1.25058,1.249025,1.25356,1.25012,

08/28/2012,1.250115,1.24656,1.257695,1.2571,

08/29/2012,1.25709,1.251895,1.25736,1.253065,

08/30/2012,1.253075,1.248785,1.25639,1.25097,

08/31/2012,1.25096,1.249375,1.263785,1.25795,

09/02/2012,1.257195,1.256845,1.258705,1.257355,

09/03/2012,1.25734,1.25604,1.261095,1.258635,

09/04/2012,1.25865,1.25264,1.262795,1.25339,

09/05/2012,1.2534,1.250195,1.26245,1.26005,

09/06/2012,1.26006,1.256165,1.26513,1.26309,

09/07/2012,1.26309,1.262655,1.281765,1.281625,

09/09/2012,1.28096,1.27915,1.281295,1.279565,

09/10/2012,1.27957,1.27552,1.28036,1.27617,

09/11/2012,1.27617,1.2759,1.28712,1.28515,

09/12/2012,1.28516,1.281625,1.29368,1.290235,

现在这几乎已经成为一种习惯,你从编写几乎所有图表通用的代码开始,不需要更多的解释(见清单 23-2)。

清单 23-2。ch23_01.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

</style>

</head>

<body>

<script type="text/javascript">

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

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

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

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 title = d3.select("svg").append("g")

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

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

title.append("text")

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

.attr("y", -30 )

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

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

.text("My candlestick chart");

</script>

</body>

</html>

因为在文件的第一列有日期类型的值,你需要定义一个解析器来设置它们的格式(见清单 23-3)。

清单 23-3。ch23_01.html

...

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

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

var parseDate = d3.time.format("%m/%d/%Y").parse;

...

烛台图表是一种通常是时间性的数据表示,即,四个 OHLC 数据与单个时间单位相关,并且它们随时间的变化沿着 x 轴是可见的。因此,您将有一个 x 轴,您必须在其上处理时间值,而在 y 轴上,您将分配一个线性刻度。在定义 x 轴时,你要确保报告的日期只显示日和月,这将由前三个字符表示(见清单 23-4)。

清单 23-4。ch23_01.html

var parseDate = d3.time.format("%m/%d/%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")

.tickFormat(d3.time.format("%d-%b"))

.ticks(5);

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

...

现在观察数据文件中的内容(清单 23-1),你可以看到五列数据,其中最后四列是数字。第一列包含必须提交给解析器的日期,而其他四列被解释为数值。此外,您需要找出所有 OHLC 数据中的最大值和最小值。在迭代函数forEach()中管理所有这些方面,如清单 23-5 所示。

清单 23-5。ch23_01.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+ ")");

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

var maxVal = -1000;

var minVal = 1000;

data.forEach(function(d) {

d.date = parseDate(d.date);

d.open = +d.open;

d.close = +d.close;

d.max = +d.max;

d.min = +d.min;

if (d.max > maxVal)

maxVal = d.max;

if (d.min < minVal)

minVal = d.min;

});

});

...

接下来,在清单 23-6 中,创建 x 和 y 的域。在 x 轴上,域将处理日期,y 域将有一个扩展,它将覆盖刚刚找到的最小值和最大值之间的所有值(minValmaxVal)。

清单 23-6。ch23_01.html

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

data.forEach(function(d) {

...

});

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

y.domain([minVal,maxVal]);

});

一旦定义好了域,你就可以用 SVG 元素和它们的标签画出 x 和 y 两个轴,如清单 23-7 所示。

清单 23-7。ch23_01.html

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

...

y.domain([minVal,maxVal]);

svg.append("g")

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

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

.call(xAxis)

svg.append("text")

.attr("class", "label")

.attr("x", w)

.attr("y", -6)

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

svg.append("g")

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

.call(yAxis);

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Dollar [$]");

});

使用 SVG 元素<line>在 OHLC 图上绘制数据(见清单 23-8)。ext线是定义高值和低值之间范围的垂直线。closeopen线是两条水平线,分别对应于打开和关闭值。

清单 23-8。ch23_01.html

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

...

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Dollar [$]")

svg.selectAll("line.ext")

.data(data)

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

.attr("class", "ext")

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

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

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

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

svg.selectAll("line.close")

.data(data)

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

.attr("class", "close")

.attr("x1", function(d) { return x(d.date)+5})

.attr("x2", function(d) { return x(d.date)-1})

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

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

svg.selectAll("line.open")

.data(data)

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

.attr("class", "open")

.attr("x1", function(d) { return x(d.date)+1})

.attr("x2", function(d) { return x(d.date)-5})

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

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

});

感谢您定义新生成元素的类的方式,您可以通过使用line类,或者使用line.openline.closeline.ext类分别定义它们,为所有三行定义 CSS 样式的属性(参见清单 23-9)。

清单 23-9。ch23_01.html

<style>

body {

font: 16px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

line.open, line.close, line.ext {

stroke: blue;

stroke-width: 2;

shape-rendering: crispEdges;

}

</style>

最后你得到的是图 23-1 所示的烛台图,和用 jqPlot 得到的那些没什么好羡慕的。

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

图 23-1。

An OHLC chart

日期格式

在处理这类利用 OHLC 数据的图表时,您总是要处理 x 轴上的时间和日期值。因此,根据这个观察,分析 D3 库如何处理这种类型的数据。

如果在前面的示例中没有用零填充日期和月份,或者报告的年份只有两位数(例如,“8/9/12”),会发生什么情况?在d3.csv()函数中,D3 无法读取这种格式的日期,因此,烛台图表就不会出现。实际上,您需要做的事情非常简单,即猜测要插入解析器的格式化程序的正确序列。对于格式化程序,我们指的是前面带有“%”符号的一组字符,它根据特定的(区分大小写)字符来表示以某种方式书写的时间单位。

var parseDate = d3.time.format("%m/%e/%y").parse;

甚至字面上表示的日期也可以用同样的方式处理。您已经见过这种日期格式:

08-Aug-12,1.238485,1.2327,1.240245,1.2372,

它可以用这个解析器来处理:

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

但是还有更复杂的情况,例如:

Monday 16 April 2012,1.238485,1.2327,1.240245,1.2372,

它可以用这个解析器来处理:

var parseDate = d3.time.format("%A %e %B %Y").parse;

不同值之间的所有分隔字符(包括空格)应该在解析器中的相同位置报告。因此,如果日期是这样定义的。。。

'8 Aug-12',1.238485,1.2327,1.240245,1.2372,

您必须在定义解析器的字符串中插入空格和引号,否则日期将无法识别。

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

您还必须记住,csv 文件中唯一不能添加的分隔字符是“,”。如果您必须插入它,您必须使用 TSV(制表符分隔值)文件。

表 23-1 包括所有可用的格式化程序。它们的组合应该覆盖任何输入大小。

表 23-1。

D3 Date and Time Formatters

| 格式程序 | 描述 | | --- | --- | | %a | 缩写的工作日名称 | | %A | 完整的工作日名称 | | %b | 缩写月份名 | | %B | 完整的月份名称 | | %c | 日期和时间,格式为“%a %b %e %H:%M:%S %Y” | | %d | 以十进制数字[01,31]形式用零填充的一个月中的某一天 | | %e | 以十进制数字[ 1,31]表示的一个月中的第几天 | | %H | 以十进制数[00,23]表示的小时(24 小时制) | | %I | 以十进制数[01,12]表示的小时(12 小时制) | | %j | 以十进制数表示的一年中的某一天[001,366] | | %m | 十进制数字形式的月份[01,12] | | %M | 十进制数形式的分钟[00,59] | | %p | 上午或下午 | | %S | 十进制数形式的秒[00,61] | | %U | 以十进制数[00,53]表示的一年中的周数(星期日是一周的第一天) | | %w | 以十进制数表示的工作日[0(星期日),6] | | %W | 以十进制数[00,53]表示的一年中的周数(星期一是一周的第一天) | | %x | 日期,作为“%m/%d/%y” | | %X | 时间,作为“%H:%M:%S” | | %y | 没有世纪作为十进制数的年份[00,99] | | %Y | 以世纪为小数的年份 | | %Z | 时区偏移量,例如“-0700” | | %% | 文字“%”字符 |

蜡烛图中的方框表示

使用 jqPlot,您还看到了显示 OHLC 数据的其他方法。例如,这种数据通常由一条垂直线和一个覆盖它一定长度的垂直方框来表示。垂直线与前面的烛台相同,它位于 OHLC 的高值和低值之间。相反,该框表示开盘价和收盘价之间的范围。此外,如果开盘价大于收盘价,盒子将是给定的颜色,但如果相反,将是另一种颜色。

您使用了包含在data_08.csv文件中的相同数据,并且从上一个示例中的代码开始,您将看到将要进行的更改。

用这三条新的线替换extopenclose:extext1ext2(见清单 23-10)。然后你必须添加代表盒子的矩形。线应该是黑色的,而当开盘价大于收盘价时,方框应该是红色的,否则,在相反的情况下,方框将是绿色的。

清单 23-10。ch23_02.html

svg.selectAll("line.ext")

.data(data)

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

.attr("class", "ext")

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

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

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

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

svg.selectAll("line.ext1")

.data(data)

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

.attr("class", "ext")

.attr("x1", function(d) { return x(d.date)+3})

.attr("x2", function(d) { return x(d.date)-3})

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

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

svg.selectAll("line.ext2")

.data(data)

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

.attr("class", "ext")

.attr("x1", function(d) { return x(d.date)+3})

.attr("x2", function(d) { return x(d.date)-3})

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

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

svg.selectAll("rect")

.data(data)

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

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

.attr("y", function(d) { return y(Math.max(d.open, d.close));})

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

return y(Math.min(d.open, d.close))-y(Math.max(d.open, d.close));})

.attr("width",6)

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

return d.open > d.close ? "darkred" : "darkgreen" ;});

});

最后一件事是在清单 23-11 中设置 CSS 样式类。

清单 23-11。ch23_02.html

<style>

body {

font: 16px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

line.ext, line.ext1, line.ext2 {

stroke: #000;

stroke-width: 1;

shape-rendering: crispEdges;

}

</style>

而图 23-2 中的图表就是结果。

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

图 23-2。

A candlestick chart

摘要

在本章中,您已经看到了在 jqPlot 库的第一部分中已经讨论过的烛台图表的类型,但是这次您使用了 D3。您已经看到了如何轻松获得类似的结果,同时保持对每个图形元素的完全控制。此外,由于这种图表使用时间数据,这里您已经深入研究了 D3 库如何管理这种类型的数据以及管理格式的各种方法。

继续关注 jqPlot 库和 D3 库在实现各种类型的图表方面的并行性,在下一章你将学习散点图和气泡图,以及如何用 D3 库实现它们。

二十四、D3 散点图和气泡图

Abstract

在本章中,您将了解散点图。每当您有一组数据对[x,y]并且想要分析它们在 xy 平面上的分布时,您都会参考这种类型的图表。因此,您将首先看到如何使用 D3 库制作这种类型的图表。在第一个示例中,您将开始读取包含多个数据系列的 TSV(制表符分隔值)文件,通过它们,您将看到如何实现散点图。

在本章中,您将了解散点图。每当您有一组数据对[x,y]并且想要分析它们在 xy 平面上的分布时,您都会参考这种类型的图表。因此,您将首先看到如何使用 D3 库制作这种类型的图表。在第一个示例中,您将开始读取包含多个数据系列的 TSV(制表符分隔值)文件,通过它们,您将看到如何实现散点图。

完成散点图后,您将看到如何使用具有特定形状的标记来表示数据点,无论是从预定义的集合中选择还是通过创建原始标记。

这类图表非常重要。它是分析数据分布的基本工具;事实上,从这些图表中,您可以找到特定的趋势(趋势线)和分组(聚类)。在这一章中,两个简单的例子将向你展示如何表示趋势线和聚类。

此外,您将看到如何通过事件处理将突出显示功能添加到图表中,以及 D3 库如何管理它。

最后,本章将以最后一个例子结束,在这个例子中,你需要用三个参数[x,y,z]来表示数据。因此,适当地修改散点图,你会发现你可以得到一个气泡图,这是一个散点图修改,以能够代表一个额外的参数。

散点图

由于有了 D3 库,您可以生成的图形表示没有限制,可以像组合砖块一样组合图形元素。散点图的创建也不例外。

你开始收集数据(见清单 24-1),这一次是以表格的形式(因此是一个 TSV 文件),你将复制并保存为一个名为data_09.tsv的文件。(参见以下注释。)

Note

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

清单 24-1。data_09.tsv

time    intensity    group

10    171.11    Exp1

14    180.31    Exp1

17    178.32    Exp1

42    173.22    Exp3

30    145.22    Exp2

30    155.68    Exp3

23    200.56    Exp2

15    192.33    Exp1

24    173.22    Exp2

20    203.78    Exp2

18    187.88    Exp1

45    180.00    Exp3

27    181.33    Exp2

16    198.03    Exp1

47    179.11    Exp3

27    175.33    Exp2

28    162.55    Exp2

24    208.97    Exp1

23    200.47    Exp1

43    165.08    Exp3

27    168.77    Exp2

23    193.55    Exp2

19    188.04    Exp1

40    170.36    Exp3

21    184.98    Exp2

15    197.33    Exp1

50    188.45    Exp3

23    207.33    Exp1

28    158.60    Exp2

29    151.31    Exp2

26    172.01    Exp2

23    191.33    Exp1

25    226.11    Exp1

60    198.33    Exp3

假设文件中包含的数据属于三个不同的实验(标记为 Exp1、Exp2 和 Exp3),每个实验应用于一个不同的对象(例如,三种发光物质),您希望在其中测量它们的发射强度如何随时间变化。读数在不同的时间重复进行。您的目标是在 xy 平面上表示这些值,以便分析它们的分布和最终属性。

观察数据,您可以看到它们由三列组成:时间、强度和组成员。这是一种典型的数据结构,可以以散点图的形式显示。您将把时间刻度放在 x 轴上,把强度值放在 y 轴上,最后通过标记的形状或颜色来识别组,这些标记将标记散点图中点的位置。

按照惯例,您可以从编写清单 24-2 中的代码开始。这段代码代表您的起始代码,因为它对于您在前面的示例中看到的几乎所有图表都是通用的,所以不需要进一步解释。

清单 24-2。ch24_01.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

</style>

</head>

<body>

<script type="text/javascript">

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

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

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

var color = d3.scale.category10();

var x = d3.scale.linear()

.range([0, w]);

var y = d3.scale.linear()

.range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

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 title = d3.select("svg").append("g")

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

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

title.append("text")

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

.attr("y", –30 )

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

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

.text("My Scatterplot");

</script>

</body>

</html>

在清单 24-3 中,你用d3.tsv()函数从 TSV 文件中读取列表数据,确保数字值被读取。在这里,即使在第一列有时间,也不需要解析,因为它们是秒,因此可以被认为是线性的。

清单 24-3。ch24_01.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+ ")");

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

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

});

});

var title = d3.select("svg").append("g")

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

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

...

同样关于域,分配非常简单,如清单 24-4 所示。此外,您将使用nice()函数,该函数对域的值进行舍入。

清单 24-4。ch24_01.html

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

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

});

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

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

});

您还添加了轴标签,在 x 轴上显示“时间”,在 y 轴上显示“强度”,如清单 24-5 所示。

清单 24-5。ch24_01.html

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

...

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

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

svg.append("g")

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

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

.call(xAxis);

svg.append("text")

.attr("class", "label")

.attr("x", w)

.attr("y", h + margin.bottom - 5)

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

.text("Time [s]");

svg.append("g")

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

.call(yAxis);

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

});

最后,你必须直接在图上画出标记。这些可以用 SVG 元素<circle>来表示。因此,散点图上显示的数据点将是半径为 3.5 像素的小点(见清单 24-6)。为了定义不同组的表示,标记以不同的颜色绘制。

清单 24-6。ch24_01.html

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

...

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 3.5)

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

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

.style("fill", function(d) { return color(d.group); });

});

现在你在散点图上有这么多彩色标记,但是没有参考它们的颜色和它们所属的组。因此,有必要添加一个图例,显示与不同颜色相关的各个组的名称(见清单 24-7)。

清单 24-7。ch24_01.html

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

...

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 3.5)

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

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

.style("fill", function(d) { return color(d.group); });

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

.data(color.domain())

.enter().append("g")

.attr("class", "legend")

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

return "translate(0," + (i * 20) + ")"; });

legend.append("rect")

.attr("x", w - 18)

.attr("width", 18)

.attr("height", 18)

.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; });

});

所有工作完成后,你得到如图 24-1 所示的散点图。

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

图 24-1。

A scatterplot showing the data distribution

标记和符号

当您想要表示散点图时,不可低估的一个方面是您想要用来表示数据点的标记的形状。毫不奇怪,D3 库为您提供了许多通过符号管理标记表示的方法。在本章中,您将了解这个主题,因为它非常适合这种图表(散点图),但不会改变它对其他类型图表(如折线图)的应用。

使用符号作为标记

D3 库提供了一组可以直接用作标记的符号。在表 24-1 中,你可以看到一个报告各种预定义符号的列表。

表 24-1。

Predefined Symbols in D3 Library

| 标志 | 描述 | | --- | --- | | 圆 | 一个圆圈 | | 十字架 | 希腊十字(或加号) | | 钻石 | 菱形 | | 平方 | 轴对齐的正方形 | | 三角形向下 | 向下的等边三角形 | | 三角形向上 | 向上的等边三角形 |

继续前面的示例,您将散点图中的点替换为用作标记的不同符号。这些符号将根据数据成员的系列(Exp1Exp2,Exp3)而变化。因此,这一次,要描述数据所属的系列,就需要标记的颜色和形状。

首先,你需要在groupMarker对象中给每个序列分配一个符号,如清单 24-8 所示。

清单 24-8。ch24_01b.html

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

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

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

var groupMarker = {

Exp1: "cross",

Exp2: "diamond",

Exp3: "triangle-down"

};

var color = d3.scale.category10();

然后,从代码中删除与点的表示有关的行(见清单 24-9)。这些行将被生成标记的其他行所替换(见清单 24-10)。

清单 24-9。ch24_01b.html

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 3.5)

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

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

.style("fill", function(d) { return color(d.group); });

实际上,您将要生成的符号只不过是预定义的 SVG 路径。你可以从清单 24-10 中通过使用append("path")函数来添加符号的事实中猜到这一点。相反,关于符号的生成,D3 库提供了一个特定的函数:d3.svg.symbol()。要显示的符号作为参数通过type()函数传递,例如,如果您想使用符号交叉使用type("cross")

然而,在这种情况下,要表示的符号是三个,并且它们取决于每个点的级数。因此,您必须通过应用于groupMarker,的函数(d)对所有数据进行迭代,这将返回对应于“十字形”、“菱形”和“向下三角形”符号的字符串。

最后,由 SVG 路径构成的符号也可以通过调整级联样式表(CSS)样式来更改。在本例中,您可以通过将fill属性设置为white来选择仅表示符号的轮廓。

清单 24-10。ch24_01b.html

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

...

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

svg.selectAll("path")

.data(data)

.enter().append("path")

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

return "translate(" + x(d.time) + "," + y(d.intensity) + ")";

})

.attr("d", d3.svg.symbol().type( function(d) {

return groupMarker[d.group];

}))

.style("fill", "white")

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

.style("stroke-width", "1.5px");

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

...

});

图 24-2 显示了用各种符号代替点的散点图。

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

图 24-2。

In a scatterplot, the series could be represented by different symbols

使用定制标记

您已经看到 D3 库的标记只不过是 SVG 路径。您可以利用这一优势,通过创建其他符号来定制您的图表,这些符号将被添加到已定义的符号中。

在互联网上,你可以找到大量的 SVG 符号;一旦你决定了使用什么样的符号,你就可以得到它的路径,以便把它添加到你的网页中。更有事业心的读者也可以决定用 SVG 编辑器编辑 SVG 符号。建议你使用 Inkscape 编辑器(见图 24-3);可以从其官方网站: http://inkscape.org 下载。或者,更简单地说,您可以从一个已经设计好的 SVG 符号开始,然后根据您的喜好对其进行修改。为此,我推荐使用 SVG Tryit 页面的这个链接: www.w3schools.com/svg/tryit.asp?filename=trysvg_path (见图 24-4 )。

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

图 24-4。

Tryit is a useful tool to preview SVG symbols in real time inserting the path

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

图 24-3。

Inkscape: a good SVG editor for generating symbols

因此,选择三个新符号(如新月、星星和火星符号)来替换默认符号。你提取它们的路径,然后插入到一个新对象的定义中,你称之为markers,,如清单 24-11 所示。

清单 24-11。ch24_01c.html

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

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

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

var markers = {

mars: "m15,7 a 7,7 0 1,0 2,2 z l 1,1 7-7m-7,0 h 7 v 7",

moon: "m15,3 a 8.5,8.5 0 1,0 0,13 a 6.5,6.5 0 0,1 0,-13",

star: "m11,1 3,9h9l-7,5.5 2.5,8.5-7.5-5-7.5,5 2.5-8.5-7-6.5h9z"

};

var groupMarker = {

...

现在你必须更新在groupMarker变量中定义的符号和组之间的关联,如清单 24-12 所示。

清单 24-12。ch24_01c.html

var groupMarker = {

Exp1: markers.star,

Exp2: markers.moon,

Exp3: markers.mars

};

当你创建 SVG 元素时,你可以做的最后一件事是改变路径的定义(见清单 24-13)。

清单 24-13。ch24_01c.html

svg.selectAll("path")

.data(data)

.enter().append("path")

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

return "translate(" + x(d.time) + "," + y(d.intensity) + ")";

})

.attr("d", function(d) { return groupMarker[d.group]; })

.style("fill", "white")

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

.style("stroke-width", "1.5px");

最后,你会得到一个散点图,报告你自己创造的或从网上下载的符号(见图 24-5 )。

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

图 24-5。

A scatterplot with a customized set of markers

添加更多功能

既然您已经学习了如何使用散点图来表示数据的分布,那么是时候介绍趋势线和聚类了。通常,详细分析数据分布中的一些点集,您可以看到它们遵循特定的趋势或倾向于聚集成簇。因此,用图形突出这一点将非常有用。在本节中,您将看到如何计算和表示线性趋势线的第一个示例。然后,您将看到第二个示例,该示例说明了如何突出显示 xy 平面中的一些集群的可能性。

趋势线

至于 jqPlot 库,其中有一个插件可以直接给出趋势线,而 D3 库不仅需要实现图形,还需要实现计算。

为简单起见,您将按照线性趋势计算一组点(一个序列)的趋势线。为此,您使用最小二乘法。这种方法确保在给定一组数据的情况下,尽可能通过最小化误差(误差的平方和)找到最符合点趋势的直线。

Note

要了解更多信息,我建议你访问 Wolfram MathWorld 的文章,网址为 http://mathworld.wolfram.com/LeastSquaresFitting.html

对于本例,您将继续使用散点图的代码,但不包括插入符号后所做的所有更改。为了避免不必要的错误和更多的重复,清单 24-14 显示了你需要用来作为这个例子的起点的代码。

清单 24-14。ch24_02.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.axis path, .axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

</style>

</head>

<body>

<script type="text/javascript">

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

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

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

var color = d3.scale.category10();

var x = d3.scale.linear()

.range([0, w]);

var y = d3.scale.linear()

.range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

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+ ")");

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

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

});

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

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

svg.append("g")

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

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

.call(xAxis);

svg.append("text")

.attr("class", "label")

.attr("x", w)

.attr("y", h + margin.bottom - 5)

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

.text("Time [s]");

svg.append("g")

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

.call(yAxis);

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 3.5)

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

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

.style("fill", function(d) { return color(d.group); });

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

.data(color.domain())

.enter().append("g")

.attr("class", "legend")

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

return "translate(0," + (i * 20) + ")";

});

legend.append("rect")

.attr("x", w - 18)

.attr("width", 18)

.attr("height", 18)

.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; });

});

var title = d3.select("svg").append("g")

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

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

title.append("text")

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

.attr("y", -30)

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

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

.text("My Scatterplot");

</script>

</body></html>

首先,在tsv()函数中定义所有用于最小二乘法的变量,如清单 24-15 所示。对于每个变量,您定义一个大小为 3 的数组,因为在您的图表中要表示三个系列。

清单 24-15。ch24_02.html

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

sumx = [0,0,0];

sumy = [0,0,0];

sumxy = [0,0,0];

sumx2 = [0,0,0];

n = [0,0,0];

a = [0,0,0];

b = [0,0,0];

y1 = [0,0,0];

y2 = [0,0,0];

x1 = [9999,9999,9999];

x2 = [0,0,0];

colors = ["","",""];

data.forEach(function(d) {

...

});

现在你利用在数据解析过程中执行的数据迭代,同时计算最小二乘法所需的所有求和(见清单 24-16)。此外,直线的表示便于确定每个系列中的最大和最小 x 值。

清单 24-16。ch24_02.html

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

...

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

for(var i = 0; i < 3; i=i+1)

{

if(d.group == "Exp"+(i+1)){

colors[i] = color(d.group);

sumx[i] = sumx[i] + d.time;

sumy[i] = sumy[i] + d.intensity;

sumxy[i] = sumxy[i] + (d.time * d.intensity);

sumx2[i] = sumx2[i] + (d.time * d.time);

n[i] = n[i] +1;

if(d.time < x1[i])

x1[i] = d.time;

if(d.time > x2[i])

x2[i] = d.time;

}

}

});

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

...

});

一旦你计算了所有的总和,就该计算清单 24-17 中的最小二乘了。由于系列是三个,您将在一个for()循环中重复计算三次。此外,在每个循环中,您直接插入 SVG 元素的创建,用于绘制与每个计算结果相对应的直线。

清单 24-17。ch24_02.html

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

...

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

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

for(var i = 0; i < 3; i = i + 1){

b[i] = (sumxy[i] - sumx[i] * sumy[i] / n[i]) /

(sumx2[i] - sumx[i] * sumx[i] / n[i]);

a[i] = sumy[i] / n[i] - b[i] * sumx[i] / n[i];

y1[i] = b[i] * x1[i] + a[i];

y2[i] = b[i] * x2[i] + a[i];

svg.append("svg:line")

.attr("class","trendline")

.attr("x1", x(x1[i]))

.attr("y1", y(y1[i]))

.attr("x2", x(x2[i]))

.attr("y2", y(y2[i]))

.style("stroke", colors[i])

.style("stroke-width", 4);

}

现在你已经完成了全部,你可以看到散点图中三条趋势线的表示,如图 24-6 所示。

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

图 24-6。

Each group shows its trendline

使用散点图时,可能需要执行聚类分析。在互联网上,有许多分析方法和算法,可以让您执行各种操作的识别和研究的集群。

聚类分析是一种分类技术,旨在识别数据分布(在这种情况下,是 xy 平面上的散点图)中的数据组(准确地说是聚类)。不同数据点在这些聚类中的分配不是事先定义的,但聚类分析的任务是确定选择和分组的标准。应尽可能区分这些聚类,在这种情况下,作为分组标准,聚类分析将基于各种数据点与代表聚类的质心点之间的距离(见图 24-7 )。

因此,这种分析的目的主要是识别数据分布中可能的相似性,在这方面,没有比散点图更合适的了,在散点图中,您可以根据聚类成员通过不同点的不同颜色来突出这些相似性。

在本节中,您将看到如何实现一个聚类分析算法,以及如何将它集成到一个散点图中。

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

图 24-7。

The cluster analysis groups a set of data points around a centroid for each cluster

k 均值算法

鉴于聚类分析的复杂性,本章将不详细讨论这一主题。您只对以不同于散点图所属系列的方式突出显示散点图的各个点感兴趣(Exp1、Exp2 和 Exp3)。在本例中,您希望根据数据点所属的分类为数据点着色。为此,您将使用一个简单的聚类分析案例:K-means 算法。首先定义要将所有数据分成的聚类数,然后为每个聚类选择一个代表点(质心)。每个数据点和三个质心之间的距离被认为是隶属度的标准。

互联网上有一些应用 K-means 方法的例子,它完全是用 JavaScript 实现的;其中我选择了一个由希瑟·亚瑟( https://github.com/harthur/clusterfck )开发的,但是你可以用任何其他的来代替它。

对于这个例子,我冒昧地修改了代码,使其尽可能简单。从包含在 TSV 文件中的数据点开始,并在散点图中表示它们,您实际上是在分析这些点在 xy 空间中是如何分布的。例如,现在您有兴趣识别这个分布中的三个不同的集群。

为此,您将应用以下算法:

Make a random choice of three data points as cluster centroids.   Iterate over all the data points in the file, assigning each of them to the cluster that has the closest centroid. At the end, you have all the data points divided into three clusters.   Within each cluster, a new centroid is calculated, which this time will not correspond to any given point but will be the “midpoint” interposed between all points in the cluster.   Recalculate steps 2 and 3 until the new centroids correspond to the previous ones (that is, the coordinates of the centroids in the xy plane remain unchanged).  

一旦算法完成,散点图中的点将具有三种不同的颜色,对应于三个不同的聚类。

请注意,在这个算法中没有优化,因此每次在浏览器中上传页面时,结果总是不同的。其实你每次得到的都是一个可能的解,而不是“最佳解”。

现在为了保持一定的模块性,您将在一个外部文件中编写集群分析的代码,您将称之为kmeans.js

首先,您将实现randomCentroids()函数,该函数将从包含在文件中的点(这里在points数组中传递)中选择k个点(在本例中,k = 3 ),将它们指定为 k 个簇的质心(参见清单 24-18)。该函数对应于算法的点 1。

清单 24-18。kmeans.js

function randomCentroids(points, k) {

var centroids = points.slice(0);

centroids.sort(function() {

return (Math.round(Math.random()) - 0.5);

});

return centroids.slice(0, k);

}:

现在,您必须将文件中包含的所有点分配给三个不同的集群。为此,您需要计算每个数据点和所讨论的质心之间的距离,因此您需要实现一个特定的函数来计算两点之间的距离。在清单 24-19 中,定义了distance()函数,它返回v1v2普通点之间的距离。

清单 24-19。kmeans.js

function distance(v1, v2) {

var total = 0;

for (var i = 0; i < v1.length; i++) {

total += Math.pow((v2[i] - v1[i]), 2);

}

return Math.sqrt(total);

};

现在您已经知道如何计算两点之间的距离,您可以实现一个函数来决定每个数据点的聚类分配,计算它与所有质心的距离并选择较小的一个。因此,您可以将closestCentroid()函数添加到代码中,如清单 24-20 所示。

清单 24-20。kmeans.js

function closestCentroid(point, centroids) {

var min = Infinity;

var index = 0;

for (var i = 0; i < centroids.length; i++) {

var dist = distance(point, centroids[i]);

if (dist < min) {

min = dist;

index = i;

}

}

return index;

}:

现在,您可以编写完整表达首次公开的算法的函数。这个函数需要两个参数,输入数据点(points)和它们将被分成的聚类数(k)(见清单 24-21)。在里面,你可以使用新实现的randomCentroids()函数选择质心(算法的第一点)。

清单 24-21。kmeans.js

function kmeans(points, k) {

var centroids = randomCentroids(points, k);

};

一旦你选择了三个质心,你可以将所有的数据点(包含在points数组中)分配给三个簇,定义assignment数组,如清单 24-22 所示(算法的第 2 点)。该数组的长度与 points 数组的长度相同,其元素的顺序对应于数据点的顺序。每个元素都包含它们所属的集群的编号。例如,如果在assignment数组的第三个元素中你有一个值 2,那么这将意味着第三个数据点属于第三个簇(簇是 0、1 和 2)。

清单 24-22。kmeans.js

function kmeans(points, k) {

var centroids = randomCentroids(points, k);

var assignment = new Array(points.length);

var clusters = new Array(k);

var movement = true;

while (movement) {

for (var i = 0; i < points.length; i++) {

assignment[i] = closestCentroid(points[i], centroids);

}

movement = false;

}

return clusters;

};

最后,通过一次选择一个聚类,您将重新计算质心,并重复整个过程,直到您总是获得相同的值。首先,如清单 24-23 所示,通过迭代器j进行迭代,一次分析一个集群。在它的内部,基于assignment数组的内容,用属于该集群的所有数据点填充assigned数组。这些值用于计算在newCentroid变量中定义的新质心。要确定它的新坐标[x,y],需要分别对该簇所有点的所有 x 和 y 值求和。然后将这些量除以点数,因此新质心的 x 和 y 值只不过是所有坐标的平均值。

要做到这一切,你需要用gi迭代器实现一个双重迭代(两个for()循环)。在g上的迭代允许你一次处理一个坐标(首先是 x,然后是 y,等等),而在i上的迭代允许你逐点求和,以便进行求和。

如果新的质心不同于先前的质心,那么再次重复将各种数据点分配给聚类,并且循环再次开始(算法的步骤 3 和 4)。

清单 24-23。kmeans.js

function kmeans(points, k) {

...

while (movement) {

for (var i = 0; i < points.length; i++) {

assignment[i] = closestCentroid(points[i], centroids);

}

movement = false;

for (var j = 0; j < k; j++) {

var assigned = [];

for (var i = 0; i < assignment.length; i++) {

if (assignment[i] == j) {

assigned.push(points[i]);

}

}

if (!assigned.length) {

continue;

}

var centroid = centroids[j];

var newCentroid = new Array(centroid.length);

for (var g = 0; g < centroid.length; g++) {

var sum = 0;

for (var i = 0; i < assigned.length; i++) {

sum += assigned[i][g];

}

newCentroid[g] = sum / assigned.length;

if (newCentroid[g] != centroid[g]) {

movement = true;

}

}

centroids[j] = newCentroid;

clusters[j] = assigned;

}

}

return clusters;

};

将聚类分析应用于散点图

结束了用于聚类分析的 JavaScript 代码之后,是时候回到 web 页面了。正如您对趋势线示例所做的那样,您将使用如清单 24-24 所示的散点图代码。这是您进行集成聚类分析所需的各种更改和添加的起点。

清单 24-24。ch24_03.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.axis path, .axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

</style>

</head>

<body>

<script type="text/javascript">

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

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

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

var color = d3.scale.category10();

var x = d3.scale.linear()

.range([0, w]);

var y = d3.scale.linear()

.range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

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+ ")");

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

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

});

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

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

svg.append("g")

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

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

.call(xAxis);

svg.append("text")

.attr("class", "label")

.attr("x", w)

.attr("y", h + margin.bottom - 5)

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

.text("Time [s]");

svg.append("g")

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

.call(yAxis);

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

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

.data(color.domain())

.enter().append("g")

.attr("class", "legend")

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

return "translate(0," + (i * 20) + ")";

});

legend.append("rect")

.attr("x", w - 18)

.attr("width", 18)

.attr("height", 18)

.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; });

});

var title = d3.select("svg").append("g")

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

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

title.append("text")

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

.attr("y", -30)

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

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

.text("My Scatterplot");

</script>

</body>

</html>

首先,您需要包含您刚刚创建的文件kmeans.js,以便使用其中定义的函数(参见清单 24-25)。

清单 24-25。ch24_03.html

...

<meta charset="utf-8">

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

< script src="./kmeans.js"></script>

<style>

body {

...

准备一个保存待分析数据的数组,并将其命名为myPoints。一旦完成,你就可以添加对kmean()函数的调用了,如清单 24-26 所示。

清单 24-26。ch24_03.html

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

var myPoints = [];

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

myPoints.push([d.time, d.intensity]);

});

var clusters = kmeans(myPoints, 3);

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

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

...

};

最后,修改 circle SVG 元素的定义,使这些元素在由kmeans()函数返回的结果的基础上被表示出来,如清单 24-27 所示。

清单 24-27。ch24_03.html

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

...

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

for(var i = 0; i < 3; i = i + 1){

svg.selectAll(".dot" + i)

.data(clusters[i])

.enter().append("circle")

.attr("class", "dot")

.attr("r", 5)

.attr("cx", function(d) { return x(d[0]); })

.attr("cy", function(d) { return y(d[1]); })

.style("fill", function(d) { return color(i); });

}

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

.data(color.domain())

.enter().append("g")

...

};

在图 24-8 中,你可以看到聚类分析后可能获得的结果之一的表示。

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

图 24-8。

The scatterplot shows one possible solution of the clustering analysis applied to the data in the TSV file

突出显示数据点

另一个你还没有在 D3 库中涉及到的功能,但是你已经在 jqPlot 库中看到了(见第十章)高亮显示和与之相关的事件。D3 库甚至允许您将此功能添加到图表中,并以与 jqPlot 库非常相似的方式处理事件。

D3 库提供了一个特殊的函数来激活或删除事件监听器:on()函数。这个函数通过链接方法直接应用于选择,通常需要两个参数:类型和侦听器。

selection.``on(``type, listener

第一个参数是你想要激活的事件的类型,它被表示为一个包含事件名称的字符串(比如mouseoversubmit等)。).第二个参数通常由一个函数组成,该函数充当侦听器,并在事件被触发时执行操作。

基于所有这些,如果您想要添加突出显示功能,您需要管理两个特定的事件:一个是当用户通过突出显示将鼠标悬停在数据点上时,另一个是当用户将鼠标从数据点上移开时,将它恢复到正常状态。这两个事件在 D3 库中定义为mouseovermouseout。现在,您必须将这些事件加入到两个不同的动作中。使用mouseover,你将扩大数据点的体积,你将增加它的颜色的鲜艳度,以进一步与其他的进行对比。相反,您将使用mouseout执行完全相反的操作,恢复原始数据点的颜色和大小。

清单 24-28 显示了应用于散点图代码的高亮功能。

清单 24-28。ch24_04.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.axis path, .axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

</style>

</head>

<body>

<script type="text/javascript">

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

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

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

var color = d3.scale.category10();

var x = d3.scale.linear()

.range([0, w]);

var y = d3.scale.linear()

.range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

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+ ")");

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

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

});

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

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

svg.append("g")

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

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

.call(xAxis);

svg.append("text")

.attr("class", "label")

.attr("x", w)

.attr("y", h + margin.bottom - 5)

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

.text("Time [s]");

svg.append("g")

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

.call(yAxis);

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

var dots = svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", 5)

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

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

.style("fill", function(d) { return color(d.group); })

.on("mouseover", function() { d3.select(this)

.style("opacity",1.0)

.attr("r", 15);

})

.on("mouseout", function() { d3.select(this)

.style("opacity",0.6)

.attr("r", 5);

}) ;

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

.data(color.domain())

.enter().append("g")

.attr("class", "legend")

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

return "translate(0," + (i * 20) + ")";

});

legend.append("rect")

.attr("x", w - 18)

.attr("width", 18)

.attr("height", 18)

.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; });

});

var title = d3.select("svg").append("g")

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

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

title.append("text")

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

.attr("y", –30)

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

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

.text("My Scatterplot");

</script>

</body>

</html>

在加载网页以查看结果之前,您需要通过设置 CSS 样式中的opacity属性来使数据点的所有颜色变暗,如清单 24-29 所示。

清单 24-29。ch24_04.html

<style>

body {

font: 16px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

.dot {

stroke: #000;

opacity: 0.6;

}

</style>

图 24-9 显示了气泡图中处于两种不同状态的多个数据点之一。在左侧,您可以看到数据点处于正常状态,而在右侧,它被突出显示。

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

图 24-9。

A bubble assumes two states: normal on the left and highlighted when moused over on the right

泡泡图

只需对之前的散点图示例进行一些修改,就可以非常容易地构建一个气泡图。首先,您需要向数据中添加一个新列。在这种情况下(见清单 24-30),您将带宽值作为最后一列添加到data_09.tsv中,并将其保存为data_10.tsv

清单 24-30。data_10.tsv

time    intensity    group    bandwidth

10    171.11    Exp1    20

14    180.31    Exp1    30

17    178.32    Exp1    10

42    173.22    Exp3    40

30    145.22    Exp2    35

30    155.68    Exp3    80

23    200.56    Exp2    10

15    192.33    Exp1    30

24    173.22    Exp2    10

20    203.78    Exp2    20

18    187.88    Exp1    60

45    180.00    Exp3    10

27    181.33    Exp2    40

16    198.03    Exp1    30

47    179.11    Exp3    20

27    175.33    Exp2    30

28    162.55    Exp2    10

24    208.97    Exp1    10

23    200.47    Exp1    10

43    165.08    Exp3    10

27    168.77    Exp2    20

23    193.55    Exp2    50

19    188.04    Exp1    10

40    170.36    Exp3    40

21    184.98    Exp2    20

15    197.33    Exp1    30

50    188.45    Exp3    10

23    207.33    Exp1    10

28    158.60    Exp2    10

29    151.31    Exp2    30

26    172.01    Exp2    20

23    191.33    Exp1    10

25    226.11    Exp1    10

60    198.33    Exp3    10

现在,数据列表中有了第三个参数,对应于新的色谱柱带宽。这个值用一个数字表示,为了读取它,你需要在数据解析中添加bandwidth变量,如清单 24-31 所示。你一定不要忘记在tsv()函数中用data_10.tsv替换 TSV 文件的名称。

清单 24-31。ch24_05.html

d3.tsv("``data_10.tsv

var myPoints = [];

data.forEach(function(d) {

d.time = +d.time;

d.intensity = +d.intensity;

d.bandwidth = +d.bandwidth;

myPoints.push([d.time, d.intensity]);

});

...

});

现在你可以通过增加半径将所有的点变成圆形区域,因为它们已经被设置为 SVG 元素<circle>,如清单 24-32 所示。这些圆的半径必须与带宽值成比例,因此可以直接分配给r属性。值 0.4 是一个校正因子,它适合在气泡图中很好地表示带宽值(在其他情况下,您将需要使用其他值作为因子)。

清单 24-32。ch24_05.html

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

...

svg.append("text")

.attr("class", "label")

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

.attr("y", 6)

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

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

.text("Intensity");

svg.selectAll(".dot")

.data(data)

.enter().append("circle")

.attr("class", "dot")

.attr("r", function(d) { return d.bandwidth * 0.4 })

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

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

.style("fill", function(d) { return color(d.group); })

.on("mouseover", function() { d3.select(this)

.style("opacity",1.0)

.attr("r", function(d) { return d.bandwidth * 0.5 });

})

.on("mouseout", function() { d3.select(this)

.style("opacity",0.6)

.attr("r", function(d) { return d.bandwidth * 0.4 });

});

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

...

});

最后但同样重要的是,您需要更新新图表的标题,如清单 24-33 所示。

清单 24-33。ch24_05.html

title.append("text")

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

.attr("y", –30 )

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

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

. text("My Bubble Chart");

而图 24-10 将是结果。

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

图 24-10。

A bubble chart

摘要

在这一章中,你简要地看到了如何用 D3 库生成气泡图和散点图。甚至在这里,你使用 jqPlot 库执行了你在本书第二部分看到的相同类型的图表。因此,您可以对这两个不同的库以及实现相同类型图表的各自方法有所了解。

在下一章中,你将实现一种你在书中还没有涉及到的图表类型:雷达图。这个表示的例子用 jqPlot 是不可行的,但是由于 D3 图形元素,它是可能实现的。因此,下一章将是一个很好的例子,说明如何利用 D3 库的潜力来开发不同于最常见的图表的其他类型的图表。