JavaScript-图表入门指南-八-

122 阅读34分钟

JavaScript 图表入门指南(八)

原文:Beginning JavaScript Charts

协议:CC BY-NC-SA 4.0

二十一、D3 条形图

Abstract

在这一章中,你将看到如何使用 D3 库来构建最常用的图表类型:条形图。作为第一个例子,您将从一个简单的条形图开始练习使用标量矢量图形(SVG)元素实现所有组件。

在这一章中,你将看到如何使用 D3 库来构建最常用的图表类型:条形图。作为第一个例子,您将从一个简单的条形图开始练习使用标量矢量图形(SVG)元素实现所有组件。

绘制条形图

在这方面,作为一个例子,我们选择用竖线来表示一些国家的收入,这样我们就可以对它们进行比较。作为类别标签,您将使用国家本身的名称。在这里,正如您对折线图所做的那样,您决定使用一个外部文件,比如包含所有数据的逗号分隔值(CSV)文件。然后,您的 web 页面将使用d3.csv()函数读取文件中包含的数据。因此,将清单 21-1 中的数据写入一个文件,并保存为data_04.csv

清单 21-1。data_04.csv

country,income

France,14

Russia,22

Japan,13

South Korea,34

Argentina,28

清单 21-2 显示了一个空白网页,作为开发条形图的起点。你必须记得在网页中包含 D3 库(更多信息见附录 A)。如果您更喜欢使用内容交付网络(CDN)服务,您可以用以下内容替换参考:

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

清单 21-2。ch21_01.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

</head>

<body>

<script type="text/javascript">

// add the D3 code here

</script>

</body>

</html>

首先,定义希望在其上表示条形图的绘图区域的大小是一个好习惯。尺寸是由wh(宽度和高度)变量指定的,但是你也必须考虑边距的空间。这些空白值必须从wh中减去,适当地限制分配给你的图表的区域(见清单 21-3)。

清单 21-3。ch21_01.html

<script type="text/javascript">

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

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

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

var color =  d3.scale.category10();

</script>

此外,如果您查看 CSV 文件中的数据(见清单 21-1),您会发现一系列五个国家及其相对值。如果你想用一种颜色来区分每个国家,就必须定义一个色标,正如你已经看到的,这可以用category10()函数来完成。

下一步是在 x 轴和 y 轴上定义一个刻度。x 轴上没有数值,而是标识原产国的字符串值。因此,对于这种类型的值,你必须定义一个顺序标度,如清单 21-4 所示。事实上,函数rangeRoundBands将作为参数传递的范围划分为离散的带,这正是您在条形图中需要的。对于 y 轴,因为它用数值表示变量,所以只需选择一个线性刻度。

清单 21-4。ch21_01.html

<script type="text/javascript">

...

var color =  d3.scale.category10();

var x = d3.scale.ordinal()

.rangeRoundBands([0, w], .1);

var y = d3.scale.linear()

.range([h, 0]);

<script type="text/javascript">

现在您需要使用d3.svg.axis()功能将两个刻度分配给相应的轴。当您处理条形图时,y 轴上报告的值不是标称值,而是它们占总值的百分比,这种情况并不罕见。所以,你可以通过d3.format()定义一个百分比格式,然后通过tickFormat()函数将它分配给 y 轴上的刻度标签(见清单 21-5)。

清单 21-5。ch21_01.html

<script type="text/javascript">

...

var y = d3.scale.linear()

.range([h, 0]);

var formatPercent = d3.format(".0%");

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.tickFormat(formatPercent);

<script type="text/javascript">

最后,是时候开始在 web 页面中创建 SVG 元素了。从清单 21-6 所示的根目录开始。

清单 21-6。ch21_01.html

<script type="text/javascript">

...

var yAxis = d3.svg.axis()

.scale(y)

orient("left")

.tickFormat(formatPercent);

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

<script type="text/javascript">

现在,要访问包含在 CSV 文件中的值,你必须使用d3.csv()函数,就像你已经做的那样,传递文件名作为第一个参数,传递包含在其中的数据的迭代函数作为第二个参数(见清单 21-7)。

清单 21-7。ch21_01.html

<script type="text/javascript">

...

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_04.csv", function(error, data) {

var sum = 0;

data.forEach(function(d) {

d.income = +d.income;

sum += d.income;

});

//insert here all the svg elements depending on data in the file

});

<script type="text/javascript">

在通过forEach()循环扫描存储在文件中的值的过程中,您要确保所有的收入值都被读取为数字值,而不是字符串:这可以通过在每个值前面加上一个加号来实现。

values = +values

与此同时,你还要计算所有收入值的总和。这个总数对你计算百分比是必要的。事实上,如清单 21-8 所示,当你为两个轴定义域时,“单一收入”/总和比率被分配给 y 轴,从而得到一个百分比域。

清单 21-8。ch21_01.html

...

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

data.forEach(function(d) {

...

});

x.domain(data.map(function(d) { return d.country; }));

y.domain([0, d3.max(data, function(d) { return d.income/sum; })]);

});

在设置了两个轴上的值之后,您可以添加清单 21-9 中相应的 SVG 元素来绘制它们。

清单 21-9。ch21_01.html

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

...

y.domain([0, d3.max(data, function(d) { return d.income/sum; })]);

svg.append("g")

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

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

.call(xAxis);

svg.append("g")

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

.call(yAxis);

});

通常,对于条形图,仅在一个轴上需要网格:显示数值的轴。因为你正在处理一个垂直条形图,所以你只在 y 轴上画网格线(见清单 21-10)。另一方面,x 轴上的网格线不是必需的,因为在离散值区域中已经有了一种分类,通常称为类别。(即使在 x 轴上有连续的值要表示,但是,为了使条形图有意义,它们在 x 轴上的范围应该划分为区间或仓。这些值在每个时间间隔的频率由 y 轴上的条形高度表示,结果是一个直方图。)

清单 21-10。ch21_01.html

...

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.tickFormat(formatPercent);

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

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

...

svg.append("g")

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

.call(yAxis);

svg.append("g")

.attr("class", "grid")

.call(yGrid);

});

由于网格仅位于 y 轴上,同样的事情也适用于相应的轴标签。为了以某种方式将图表的组件分开,最好为每个 SVG 元素定义一个变量<g>,它标识一个图表组件,通常与您用来标识组件类的名称相同。因此,正如您定义一个labels变量一样,您也定义了一个title变量。对于您打算添加的所有其他组件,以此类推。

与 jqPlot 不同,不需要包含特定的插件来旋转轴标签;相反,使用 SVG 提供的一种可能的变换,更具体地说是旋转。您唯一需要做的事情就是传递想要旋转 SVG 元素的角度(以度为单位)。如果传递的值为正值,则旋转为顺时针方向。如果,就像在你的例子中,你想将轴标签与 y 轴对齐,那么你需要将它逆时针旋转 90 度:所以指定rotate(90)作为转换(见清单 21-11)。关于title元素,你把它放在你图表的顶部,在中心位置。

清单 21-11。ch21_01.html

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

...

svg.append("g")

.attr("class", "grid")

.call(yGrid);

var labels = svg.append("g")

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

labels.append("text")

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

.attr("y", 6)

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

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

.text("Income [%]");

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("My first bar chart");

});

一旦您定义了所有的 SVG 组件,您一定不要忘记在清单 21-12 中指定 CSS 类的属性。

清单 21-12。ch21_01.html

<style>

body {

font: 14px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

.grid .tick {

stroke: lightgrey;

opacity: 0.7;

}

.grid path {

stroke-width: 0;

}

.x.axis path {

display: none;

}

</style>

最后,添加 SVG 元素来组成你的条。因为您想要为每组数据绘制一个条形,所以这里您必须利用d3.csv()函数中的迭代function(error,data)函数。如清单 21-13 所示,您因此在函数链中添加了data(data)enter()函数。此外,在每个attr()函数中定义function(d),您可以迭代地将数据值一个接一个地分配给相应的属性。通过这种方式,您可以为属性xyheightfill分配不同的值(CSV 文件中的每一行一个值),这会影响每个条形的位置、颜色和大小。通过这种机制,每个.bar元素将反映 CSV 文件中某一行包含的数据。

清单 21-13。ch21_01.html

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

...

title.append("text")

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

.attr("y", -30 )

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

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

.text("My first bar chart");

svg.selectAll(".bar")

.data(data)

.enter().append("rect")

.attr("class", "bar")

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

.attr("width", x.rangeBand())

.attr("y", function(d) { return y(d.income/sum); })

.attr("height", function(d) { return h - y(d.income/sum); })

.attr("fill", function(d) { return color(d.country); });

});

最后,你所有的努力都会得到如图 21-1 所示的美丽条形图的回报。

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

图 21-1。

A simple bar chart

绘制堆积条形图

您已经用最简单的例子介绍了条形图,其中每个国家有许多组,并有相应的值(收入)。通常,您需要表示稍微复杂一点的数据,例如,您想要按部门划分总收入的数据。在这种情况下,您会将每个国家的收入分成不同的部分,每个部分代表一个生产部门的收入。在我们的例子中,我们使用 CSV 文件的方式与上一个例子非常相似(见清单 21-1),但是每个国家有多个值,这样你就可以使用多系列条形图。因此,用文本编辑器编写清单 21-14 中的数据,并保存为data_05.csv

清单 21-14。data_05.csv

Country,Electronics,Software,Mechanics

Germany,12,14,18

Italy,8,12,10

Spain,6,4,5

France,10,14,9

UK,7,11,9

查看文件的内容,您可能会注意到现在有四列。第一列仍然包含国家的名称,但是现在收入是三个,每个对应不同的生产部门:电子、软件和机械。这些标题列在标题中。

从上一个例子的代码开始,做一些修改并删除一些行,直到得到如清单 21-15 所示的代码。粗体显示的代码段是需要更改的代码段(标题和 CSV 文件),而不存在的代码段必须删除。那些直接从本节开始的人可以很容易地复制清单 21-15 中的内容。

清单 21-15。ch21_02.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 14px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

.x.axis path {

display: none;

}

</style>

</head>

<body>

<script type="text/javascript">

var color =  d3.scale.category10();

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

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

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

var x = d3.scale.ordinal()

.rangeRoundBands([0, w], .1);

var y = d3.scale.linear()

.range([h, 0]);

var formatPercent = d3.format(".0%");

var xAxis = d3.svg.axis()

.scale(x)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left")

.tickFormat(formatPercent);

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

d3.csv("data_05.csv", 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")

.call(yGrid);

});

var labels = svg.append("g")

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

labels.append("text")

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

.attr("x", 50)

.attr("y", -20)

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

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

.text("Income [%]");

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 stacked bar chart");

</script>

</body>

</html>

现在想想你想要设置的颜色和域。在前一个案例中,您用不同的颜色绘制了每个条形(国家)。事实上,你甚至可以给所有的条都同样的颜色。你的方法是一个可选的选择,主要是由于美学因素。但是,在这种情况下,需要使用一组不同的颜色来区分组成每个条形的各个部分。因此,每个条形都有一系列相同的颜色,每种颜色对应一个生产部门。一个小提示:当您需要一个图例来标识数据的各种表示时,您需要使用一系列颜色,反之亦然。您可以根据文件头定义颜色域,并通过过滤器删除第一项“国家”(参见清单 21-16)。

清单 21-16。ch21_02.html

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

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

return key !== "Country"; }));

svg.append("g")

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

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

.call(xAxis);

...

在 y 轴上,你不能画出收入的数值,而是它们占总收入的百分比。为了做到这一点,您需要知道所有收入值的总和,因此通过迭代从文件中读取的所有数据,您可以获得总和。同样,为了让 D3 明白三列(电子、机械和软件)中的值是数值,您必须在迭代中以如下方式明确指定它们:

values = +values;

在清单 21-17 中,你可以看到forEach()函数是如何迭代文件的值的,同时,计算出你需要得到的百分比的总和。

清单 21-17。ch21_02.html

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

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

return key !== "Country"; }));

var sum= 0;

data.forEach(function(d){

d.Electronics = +d.Electronics;

d.Mechanics = +d.Mechanics;

d.Software = +d.Software;

sum = sum +d.Electronics +d.Mechanics +d.Software;

});

svg.append("g")

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

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

.call(xAxis);

...

现在你需要创建一个数据结构来满足你的需求。为每个条形构建一个对象数组,其中每个对象对应于总收入被划分的部分之一。将这个数组命名为"countries”,并通过一个迭代函数来创建它(见清单 21-18)。

清单 21-18。ch21_02.html

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

...

data.forEach(function(d){

d.Electronics = +d.Electronics;

d.Mechanics = +d.Mechanics;

d.Software = +d.Software;

sum = sum +d.Electronics +d.Mechanics +d.Software;

});

data.forEach(function(d) {

var y0 = 0;

d.countries = color.domain().map(function(name) {

return {name: name, y0: y0/sum, y1: (y0 += +d[name])/sum }; });

d.total = d.countries[d.countries.length - 1].y1;

});

svg.append("g")

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

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

.call(xAxis);

...

使用 Firebug 控制台(参见第一章中的“Firebug 和 DevTool”一节),你可以直接看到这个数组的内部结构。因此,将对控制台的调用(临时)添加到将countries数组作为参数传递的代码中,如清单 21-19 所示。

清单 21-19。ch21_02.html

data.forEach(function(d) {

var y0 = 0;

d.countries = color.domain().map(function(name) {

return {name: name, y0: y0/sum, y1: (y0 += +d[name])/sum }; });

d.total = d.countries[d.countries.length - 1].y1;

console.log(d.countries);

});

图 21-2 显示了countries数组的内部结构及其所有内容,以及 Firebug 控制台是如何显示的。

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

图 21-2。

The Firebug console shows the content and the structure of the countries array

如果您详细分析数组的第一个元素:

[Object { name="Electronics",  y0=0,  y1=0.08053691275167785},

Object { name="Software",  y0=0.08053691275167785,  y1=0.174496644295302},

Object { name="Mechanics",  y0=0.174496644295302,  y1=0.2953020134228188}]

您可能会注意到,数组的每个元素依次是一个包含三个对象的数组。这三个对象代表要将数据拆分成的三个类别(多系列条形图的三个系列)。y0y1的值分别是条形中每个部分的开始和结束的百分比。

在你整理好所有你需要的数据后,你可以把它包含在 x 和 y 的域中,如清单 21-20 所示。

清单 21-20。ch21_02.html

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

...

data.forEach(function(d) {

...

console.log(d.countries);

});

x.domain(data.map(function(d) { return d.Country; }));

y.domain([0, d3.max(data, function(d) { return d.total; })]);

svg.append("g")

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

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

.call(xAxis);

...

然后,在清单 21-21 中,你开始定义构成图表条的rect元素。

清单 21-21。ch21_02.html

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

...

svg.append("g")

.attr("class", "grid")

.call(yGrid);

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

.data(data)

.enter().append("g")

.attr("class", "country")

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

return "translate(" + x(d.Country) + ",0)"; });

country.selectAll("rect")

.data(function(d) { return d.countries; })

.enter().append("rect")

.attr("width", x.rangeBand())

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

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

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

});

您已经看到了带有库的数组的内部结构,由于 D3 总是从基本图形开始,所以最复杂的部分在于将数据结构转换成 SVG 元素的层次结构。标签有助于您建立适当的层次分组。在这方面,您必须为每个国家定义一个元素<g>。首先,您需要迭代地使用从 CSV 文件中读取的数据。这可以通过将data数组(原来的data数组,而不是您刚刚定义的countries数组)作为参数传递给data()函数来实现。当所有这些完成后,您有五个新的组项目<g>,因为五个是 CSV 文件中列出的国家,五个也是将要绘制的条。还应该管理每个条形在 x 轴上的位置。您不需要做任何计算来将正确的 x 值传递给translate(x,0)函数。事实上,如图 21-3 所示,这些值是由 D3 自动生成的,利用了您已经在 x 轴上定义了一个序数刻度的事实。

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

图 21-3。

Firebug shows the different translation values on the x axis that are automatically generated by the D3 library

在每个组元素<g>中,您现在必须创建<rect>元素,这将为每个部分生成彩色矩形。此外,有必要确保将正确的值分配给yheight属性,以便正确地将矩形一个放置在另一个之上,避免它们重叠,从而为每个国家获得单个堆叠条形图。

这一次,将使用countries数组,将其作为参数传递给data()函数。由于有必要对您创建的每个元素<g>进行进一步的迭代,您将把迭代function(d)作为参数传递给data()函数。这样,你在另一个迭代中创建一个迭代:第一个扫描data (countries)中的值;第二个内部函数扫描countries数组中的值(生产部门)。因此,您将最终百分比(y1)分配给y属性,并将初始百分比和最终百分比之间的差值(y0–y1)分配给height属性。当您逐个定义包含在国家数组中的对象时,值 y0 和 y1 已经在前面计算过了(参见图 21-4 )。

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

图 21-4。

Firebug shows the different height values attributed to each rect element

最后,您可以欣赏图 21-5 中的堆积条形图。

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

图 21-5。

A stacked bar chart

看着你的堆积条形图,你马上会发现少了点什么。你如何识别生产部门,他们的参考色是什么?为什么不加个图例?

正如您对其他图表组件所做的那样,您可能更喜欢为这个新组件定义一个legend变量。一旦创建了组元素<g>,图例也需要一次迭代(见清单 21-22)。迭代必须在生产部门进行。对于每个项目,您需要获取扇区的名称和相应的颜色。为此,这次您将利用您之前定义的颜色域:对于text元素,您将使用 CSV 文件中的标题,而对于颜色,您将直接分配域的值。

清单 21-22。ch21_02.html

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

...

country.selectAll("rect")

.data(function(d) { return d.countries; })

.enter().append("rect")

.attr("width", x.rangeBand())

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

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

.style("fill", 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; });

});

为了完成堆叠条形图的主题,使用 D3 库,通过添加清单 21-23 中的单行,可以以降序来表示条形图。尽管在您的情况下您并不真正需要这个特性,但是在某些特定的情况下它可能是有用的。

清单 21-23。ch21_02.html

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

...

data.forEach(function(d) {

...

console.log(d.countries);

});

data.sort(function(a, b) { return b.total - a.total; });

x.domain(data.map(function(d) { return d.Country; }));

...

});

图 21-6 显示了沿 x 轴降序排列的堆积条形图。

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

图 21-6。

A sorted stacked bar chart with a legend

标准化堆积条形图

在本节中,您将看到如何在规范化图表中转换前面的图表。所谓“标准化”,我们的意思是要在图表中显示的值的范围被转换成另一个目标范围,如果你谈论的是百分比,它通常从 0 到 1,或者从 0 到 100(一个非常相似的概念在第一章 9 的“范围、域和标度”一节中被处理)。因此,如果您想要比较包含彼此差异很大的数值范围的不同系列,您需要执行归一化,以 0 到 100(或 0 到 1)之间的百分比值报告所有这些区间。事实上,在比较多个数据系列时,我们通常对它们的相对特征感兴趣。例如,在我们的例子中,您可能对机械部门如何影响一个国家的经济收入(标准化)感兴趣,也可能对比较这种影响如何因国家而异(标准化值之间的比较)感兴趣。因此,为了响应这样的需求,您可以用规范化的格式表示堆积图。

您已经报告了 y 轴上的百分比值;然而,每个生产部门的百分比是相对于所有国家的收入总额计算的。这一次,百分比将根据每个国家的收入来计算。因此,在这种情况下,你不关心每个单独的部分如何分享全球收入(指所有五个国家),但你只关心每个部门在各自国家产生的收入的百分比。因此,在这种情况下,每个国家将由 100%的条形表示。现在,没有哪个国家的收入比其他国家多的信息,但是你只对每个国家内部的信息感兴趣。

所有这些推理对你来说都很重要,你要明白,尽管从相同的数据开始,你需要选择不同类型的图表,这取决于你想让那些看图表的人注意到什么。

对于这个例子,你将使用同一个文件data_05.csv(参见清单 21-14);正如我们刚才所说的,传入的信息是相同的,但它的解释是不同的。为了规范化前面的堆积条形图,您需要对代码进行一些更改。如清单 21-24 所示,首先将左边距和右边距延长几个像素。

清单 21-24。ch21_03.html

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

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

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

在清单 21-25 中,在d3.csv()函数中,你必须消除计算总收入的迭代,这是不再需要的。相反,您可以添加一个新的迭代,将每个国家的百分比考虑在内。然后,你必须消除 y 域的定义,只留下 x 域。

清单 21-25。ch21_03.html

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

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

return key !== "Country"; }));

data.forEach(function(d) {

var y0 = 0;

d.countries = color.domain().map(function(name) {

return {name: name, y0: y0, y1: y0 += +d[name]}; });

d.countries.forEach(function(d) { d.y0 /= y0; d.y1 /= y0; });

});

x.domain(data.map(function(d) { return d.Country; }));

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

...

在这种新型图表中,y 标签将被条形覆盖。因此,您必须删除或注释掉rotate()函数,以使它再次可见,如清单 21-26 所示。

清单 21-26。ch21_03.html

labels.append("text")

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

.attr("x", 50)

.attr("y", -20)

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

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

.text("Income [%]");

当你在做的时候,为什么不抓住机会改变你的图表的标题呢?因此,修改标题,如清单 21-27 所示。

清单 21-27。ch21_03.html

title.append("text")

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

.attr("y", -30 )

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

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

.text("A normalized stacked bar chart");

甚至不再需要图例。事实上,你可以用另一种功能非常相似的图形来代替它。因此,您可以从代码中删除定义清单 21-28 中图例的行。

清单 21-28。ch21_03.html

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

现在您已经删除了图例并做了正确的更改,如果您加载网页,您将得到图 21-7 中的标准化堆积条形图。

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

图 21-7。

A normalized stacked bar chart

如果没有图例,你必须再一次知道,以某种方式,条形中的颜色指的是什么;您将使用报告组名称的标签来标记右边的最后一个条形。

首先在清单 21-29 中添加一个新的样式类。

清单 21-29。ch21_03.html

<style>

...

.x.axis path {

display: none;

}

.legend line {

stroke: #000;

shape-rendering: crispEdges;

}

</style>

因此,代替你刚刚删除的代码,如清单 21-28 所示,你添加清单 21-30 中的代码。

清单 21-30。ch21_03.html

country.selectAll("rect")

.data(function(d) { return d.countries; })

.enter().append("rect")

.attr("width", x.rangeBand())

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

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

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

var legend = svg.select(".country:last-child")

.data(data);

legend.selectAll(".legend")

.data(function(d) { return d.countries; })

.enter().append("g")

.attr("class", "legend")

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

return "translate(" + x.rangeBand()*0.9 + "," +

y((d.y0 + d.y1) / 2) + ")";

});

legend.selectAll(".legend")

.append("line")

.attr("x2", 10);

legend.selectAll(".legend")

.append("text")

.attr("x", 13)

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

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

});

当您将标签添加到最后一个条时,定义它们的 SVG 元素必须属于与最后一个国家对应的组。因此,您使用.country: last-child选择器来获取包含所有条的选择的最后一个元素。因此,新的图表看起来将如图 21-8 所示。

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

图 21-8。

A normalized stacked bar chart with labels as legend

绘制分组条形图

总是使用包含在data_05.csv中的相同数据,您可以获得另一种表示:分组条形图。当你想关注每个生产部门的个人收入时,这种表示法是最合适的。在这种情况下,你并不关心这些部门在总收入中所占的比重。因此,百分比消失,取而代之的是写入 CSV 文件的 y 值。

清单 21-31 显示的部分代码几乎可以与前面的例子相媲美,所以我们不会详细讨论它。事实上,您将使用它作为添加其他代码片段的起点。

清单 21-31。ch21_04.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 14px sans-serif;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

shape-rendering: crispEdges;

}

.x.axis path {

display: none;

}

</style>

</head>

<body>

<script type="text/javascript">

var color =  d3.scale.category10();

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

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

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

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

d3.csv("data_05.csv", 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")

.call(yGrid);

});

</script>

</body>

</html>

为了这个特定的目的,你需要在 x 轴上定义两个不同的变量:x0x1,两者都遵循一个顺序标度,如清单 21-32 所示。x0标识所有条形组的顺序刻度,代表一个国家,而x1是每个组内每个单个条形的顺序刻度,代表一个生产部门。

清单 21-32。ch21_04.html

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

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

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

var x0 = d3.scale.ordinal()

.rangeRoundBands([0, w], .1);

var x1 = d3.scale.ordinal();

var y = d3.scale.linear()

.range([h, 0]);

...

因此,在轴的定义中,你将 x0 赋给 x 轴,y 赋给 y 轴(见清单 21-33)。取而代之的是,变量 x1 将在以后仅被用作表示单个条的参考。

清单 21-33。ch21_04.html

...

var y = d3.scale.linear()

.range([h, 0]);

var xAxis = d3.svg.axis()

.scale(x0)

.orient("bottom");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

...

d3.csv()函数中,用keys()函数提取所有生产部门的名称,用filter()函数从数组中过滤掉“国家”标题,如清单 21-34 所示。这里,您也为每个国家构建了一个对象数组,但是结构略有不同。新数组如下所示:

[Object { name="Electronics",  value=12},

Object { name="Software",  value=14},

Object { name="Mechanics",  value=18}]

清单 21-34。ch21_04.html

...

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

var sectorNames = d3.keys(data[0]).filter(function(key) {

return key !== "Country"; });

data.forEach(function(d) {

d.countries = sectorNames.map(function(name) {

return {name: name, value: +d[name]

};

});

...

});

一旦定义了数据结构,就可以定义新的域,如清单 21-35 所示。

清单 21-35。ch21_04.html

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

...

data.forEach(function(d) {

...

});

x0.domain(data.map(function(d) { return d.Country; }));

x1.domain(sectorNames).rangeRoundBands([0, x0.rangeBand()]);

y.domain([0, d3.max(data, function(d) {

return d3.max(d.countries, function(d) { return d.value; });

})]);

svg.append("g")

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

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

.call(xAxis);

...

如前所述,使用x0您可以指定每个国家名称的有序域名。相反,在x1中,各个扇区的名称组成了域名。最后,在y中,域是由数值定义的。用新域更新迭代中传递的值(见清单 21-36)。

清单 21-36。ch21_04.html

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

...

svg.append("g")

.attr("class", "grid")

.call(yGrid);

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

.data(data)

.enter().append("g")

.attr("class", "country")

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

return "translate(" + x0(d.Country) + ",0)";

});

country.selectAll("rect")

.data(function(d) { return d.countries; })

.enter().append("rect")

.attr("width", x1.rangeBand())

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

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

.attr("height", function(d) { return h - y(d.value); })

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

});

然后,在csv()函数的外部,你可以定义 SVG 元素,它将代表 y 轴上的轴标签,如清单 21-37 所示。它不需要在csv()函数中定义,因为它独立于 CSV 文件中包含的数据。

清单 21-37。ch21_04.html

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

...

});

var labels = svg.append("g")

.attr("class","labels")

labels.append("text")

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

.attr("y", 5)

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

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

.text("Income");

最后一件事。。。您需要给图表添加一个合适的标题,如清单 21-38 所示。

清单 21-38。ch21_04.html

labels.append("text")

...

.text("Income");

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 grouped bar chart");

而图 21-9 就是结果。

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

图 21-9。

A grouped bar chart

在前面的例子中,使用规范化条形图,您看到了表示图例的另一种方法。您已经通过在最后一个条上放置一些报告系列名称的标签建立了这个图例(参见图 21-8 )。实际上你使用了点标签。这些标签可以包含任何文本,并直接连接到图表中的单个值。此时,引入点标签。您将把它们放在每个条形的顶部,显示该条形所表示的数值。这大大增加了每种图表的可读性。

正如您对任何其他图表组件所做的那样,定义了PointLabels变量后,您可以使用它来分配应用于相应选择的函数链。此外,对于这种类型的组件,它具有针对单个数据的特定值,您可以利用 CSV 文件中包含的数据的迭代。您想要迭代的数据与您用于条形图的数据相同。因此,你将同样的迭代function(d)作为参数传递给data()函数(见清单 21-39)。为了在条形顶部绘制数据,您将为每个PointLabel应用一个translate()转换。

清单 21-39。ch21_04.html

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

...

country.selectAll("rect")

...

.attr("height", function(d) { return h - y(d.value); })

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

var pointlabels = country.selectAll(".pointlabels")

.data(function(d) { return d.countries; })

.enter().append("g")

.attr("class", "pointlabels")

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

return "translate(" + x1(d.name) + "," + y(d.value) + ")";

})

.append("text")

.attr("dy", "-0.3em")

.attr("x", x1.rangeBand()/2)

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

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

...

});

最后,除了向图表中添加一个以经典格式分组的图例之外,没有什么要做的了(见清单 21-40)。

清单 21-40。ch21_04.html

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

...

pointlabels.append("text")

...

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

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

});

图 21-10 显示了带有点标签和线段图例的新图表。

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

图 21-10。

A grouped bar chart reporting the values above each bar

带有负值的水平条形图

到目前为止你只使用了正值,但是如果你同时有正值和负值呢?你如何用条形图来表示它们呢?例如,这个包含正值和负值的值序列(见清单 21-41)。

清单 21-41。ch21_05.html

var data = [4, 3, 1, -7, -10, -7, 1, 5, 7, -3, -5, -12, -7, -11, 3, 7, 8, -1];

在分析要显示的数据之前,开始给图表添加页边空白,如清单 21-42 所示。

清单 21-42。ch21_05.html

var data = [4, 3, 1, -7, -10, -7, 1, 5, 7, -3, -5, -12, -7, -11, 3, 7, 8, -1];

var margin = {top: 30, right: 10, bottom: 10, left: 30},

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

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

在这种特殊情况下,您将使用水平条,输入数组中的值将在 x 轴上表示,值 0 位于中间。为了实现这一点,首先需要找到绝对值的最大值(包括负值和正值)。然后在线性标度上创建 x 变量,而 y 变量被赋给一个包含数据在输入数组中放置顺序的序数标度(见清单 21-43)。

清单 21-43。ch21_05.html

...

var margin = {top: 30, right: 10, bottom: 10, left: 30},

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

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

var xMax = Math.max(-d3.min(data), d3.max(data));

var x = d3.scale.linear()

.domain([-xMax, xMax])

.range([0, w])

.nice();

var y = d3.scale.ordinal()

.domain(d3.range(data.length))

.rangeRoundBands([0, h], .2);

在清单 21-44 中,你将两个刻度分配给相应的 x 轴和 y 轴。这一次,x 轴将绘制在图表的上部,而 y 轴将向下(y 值向下增长)。

清单 21-44。ch21_05.html

var y = d3.scale.ordinal()

.domain(d3.range(data.length))

.rangeRoundBands([0, h], .2);

var xAxis = d3.svg.axis()

.scale(x)

.orient("top");

var yAxis = d3.svg.axis()

.scale(y)

.orient("left");

此时,除了开始实现绘图区域之外,没有什么要做的了。创建根<svg>元素,指定先前定义的边距。然后,定义 x 轴和 y 轴(见清单 21-45)。

清单 21-45。ch21_05.html

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

svg.append("g")

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

.call(xAxis);

svg.append("g")

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

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

.call(yAxis);

最后,您需要为每个要表示的条形插入一个<rect>元素,小心地将条形分成两个不同的组:负条形和正条形(见清单 21-46)。必须区分这两个类别,以便使用 CSS 样式类(例如,颜色)分别设置它们的属性。

清单 21-46。ch21_05.html

svg.append("g")

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

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

.call(yAxis);

svg.selectAll(".bar")

.data(data)

.enter().append("rect")

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

return d < 0 ? "bar negative" : "bar positive";

})

.attr("x", function(d) { return x(Math.min(0, d)); })

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

.attr("width", function(d) { return Math.abs(x(d) - x(0)); })

.attr("height", y.rangeBand());

事实上,如果你用 Firebug 分析图 21-11 中的结构,你会发现这个迭代在同一个组中创建了两种不同类型的条形,可以通过类名“正条形”和“负条形”的特征来识别通过这两个不同的名称,您可以应用两种不同的 CSS 样式来区分具有负值和正值的条形。

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

图 21-11。

Firebug shows how it is possible to distinguish the positive from the negative bars, indicating the distinction in the class of each rect element

根据我们刚才所说的,你为正负条形设置了样式类属性,如清单 21-47 所示。

清单 21-47。ch21_05.html

<style>

.bar.positive {

fill: red;

stroke: darkred;

}

.bar.negative {

fill: lightblue;

stroke: blue;

}

.axis path,

.axis line {

fill: none;

stroke: #000;

}

body {

font: 14px sans-serif;

}

</style>

最后,你会得到图 21-12 中的图表,红色条代表正值,蓝色条代表负值。

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

图 21-12。

A horizontal bar chart

摘要

在这一章中,你已经涵盖了几乎所有与条形图实现相关的基本方面,这种类型的图表是在本书的第一部分中使用 jqPlot 库开发的。在这里,您使用了 D3 库。因此,您看到了如何逐个元素地实现一个简单的条形图;然后,您转到堆叠条形图和分组条形图的各种情况,最后看一个最特殊的情况:描绘负值的水平条形图。

在下一章中,您将继续使用相同的方法:您将学习如何以类似于使用 jqPlot 时所用的方式实现饼状图,但是这一次您将使用 D3 库。

二十二、D3 饼图

Abstract

在前一章中,你已经看到了条形图是如何表示某一类数据的。您还看到,从相同的数据结构开始,根据您的意图,您可以选择一种类型的图表,而不是另一种,以便强调数据的特定方面。例如,在选择标准化堆积条形图时,您希望关注每个部门在其所在国家产生的收入百分比。

在前一章中,你已经看到了条形图是如何表示某一类数据的。您还看到,从相同的数据结构开始,根据您的意图,您可以选择一种类型的图表,而不是另一种,以便强调数据的特定方面。例如,在选择标准化堆积条形图时,您希望关注每个部门在其所在国家产生的收入百分比。

通常,用条形图表示的数据也可以用饼图表示。在这一章中,你将学习如何使用 D3 库创建这种类型的图表。假设这个库不像 jqPlot 那样提供已经实现的图形,而是要求用户使用基本的标量矢量图形(SVG)元素来构建它们,那么我们将从如何构建圆弧和扇形开始。事实上,就像条形图的矩形和折线图的线条一样,如果您要实现饼图(使用扇形)或圆环图(使用弧线),这些形状是非常重要的。在您实现了饼图的经典示例之后,我们将通过创建一些变体来进一步深化这个主题。在本章的第二部分,您将处理圆环图,管理从逗号分隔值(CSV)文件中读取的多个数据系列。

最后,我们将用一张图来结束这一章,这张图我们还没有处理过:极区图。这种类型的图表是饼图的进一步发展,其中的切片不再包含在一个圆圈中,而是具有不同的半径。有了极区图,信息将不再仅仅由切片所占据的角度来表示,而是由它的半径来表示。

基本饼图

为了更好地突出条形图和饼图之间的相似之处,在本例中,您将使用与创建基本条形图相同的 CSV 文件(参见第二十一章中的“绘制条形图”一节)。因此,在本节中,您的目的是使用相同的数据实现相应的饼图。为了做到这一点,在你开始“烘烤”馅饼和油炸圈饼之前,你必须首先获得正确形状的“烤盘”。D3 库还允许您表示弧形,如拱形和扇形,尽管实际上没有这样的 SVG 元素。事实上,您很快就会看到,由于 D3 的一些方法,它可以像处理其他真正的 SVG 元素(矩形、圆形、直线等)一样处理圆弧和扇形。).一旦你对这些元素的实现有了信心,你创建一个基本的饼图的工作就差不多完成了。在这一部分的第二部分,你将制作一些主题的变化,主要是形状边框和颜色。

绘制一个基本的饼图

将注意力再次转向包含在名为data_04.csv的 CSV 文件中的数据(参见清单 22-1)。

清单 22-1。data_04.csv

country,income

France,14

Russia,22

Japan,13

South Korea,34

Argentina,28

现在,我们将演示这些数据如何很好地适应饼图表示。首先,在清单 22-2 中,定义了绘图区域和边距。

清单 22-2。ch22_01a.html

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

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

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

即使对于饼图,您也需要使用一系列颜色来区分它们之间的切片。一般来说,通常使用category10()函数来创建一个颜色域,这就是你到目前为止所做的。在这个例子中,您可以做同样的事情,但是这并不总是必需的。因此,我们利用这个例子来看看如何传递自定义颜色序列。通过定义你喜欢的颜色来创建一个定制的例子,一个接一个,如清单 22-3 所示。

清单 22-3。ch22_01a.html

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

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

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

var color = d3.scale.ordinal()

. range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);

以前你用rect元素构建了条,而现在你必须处理圆的截面。因此,你正在处理圆、角、拱、半径等。在 D3 里,有一整套工具可以让你处理这些类型的对象,让你处理饼状图更加容易。

为了表示饼图的切片(圆形扇区),D3 为您提供了一个函数:d3.svg.arc()。这个函数实际上定义了拱门。术语“弧”是指由一个角度和两个圆界定的特定几何表面,一个圆具有较小的半径(内半径),另一个圆具有较大的半径(外半径)。扇形,即饼图的切片,只不过是一个内径等于 0 的圆弧(见图 22-1 )。

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

图 22-1。

By increasing the inner radius, it is possible to switch from a circle sector to an arc

首先,计算与绘图区域大小一致的半径。然后,根据这个范围,你划定了外半径和内半径,在这个例子中是 0(见清单 22-4)。

清单 22-4。ch22_01a.html

...

var color = d3.scale.ordinal()

.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);

var radius = Math.min(w, h) / 2;

var arc = d3.svg.arc()

.outerRadius(radius)

.innerRadius(0);

D3 还提供了一个定义饼图的函数:d3.layout.pie()函数。此函数构建了一个布局,允许您以非常简单的方式计算弧的开始和结束角度。使用这样的函数不是强制性的,但是饼图布局会自动将数据数组转换为对象数组。因此,定义一个对收入值有迭代函数的饼图,如清单 22-5 所示。

清单 22-5。ch22_01a.html

...

var arc = d3.svg.arc()

.outerRadius(radius)

.innerRadius(0);

var pie = d3.layout.pie()

.sort(null)

.value(function(d) { return d.income; });

现在,如清单 22-6 所示,插入根元素<svg>,分配正确的维度和适当的translate()转换。

清单 22-6。ch22_01a.html

...

var pie = d3.layout.pie()

.sort(null)

.value(function(d) { return d.income; });

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(" +(w / 2 + margin.left) +

"," + (h / 2 + margin.top) + ")");

接下来,为了读取 CSV 文件中的数据,您将一如既往地使用d3.csv()函数。在这里,您也必须确保收入是用数值而不是字符串来解释的。然后,用forEach()函数编写迭代,并在收入值旁边加上“+”号,如清单 22-7 所示。

清单 22-7。ch22_01a.html

...

.append("g")

.attr("transform", "translate(" +(w/2+margin.left)+

"," +(h/2+margin.top)+ ")");

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

data.forEach(function(d) {

d.income = +d.income;

});

});

现在是时候添加一个<arc>项了,但是这个元素并不作为 SVG 元素存在。事实上,这里使用的是一个描述弧线形状的<path>元素。正是 D3 本身通过pie()arc()函数构建了相应的路径。这使你免去了一项实在太复杂的工作。你只剩下定义这些元素的任务,就像它们是<arc>元素一样(见清单 22-8)。

清单 22-8。ch22_01a.html

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

data.forEach(function(d) {

d.income = +d.income;

});

var g = svg.selectAll(".arc")

.data(pie(data))

.enter().append("g")

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

g.append("path")

.attr("d", arc)

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

});

如果你用 Firebug 分析 SVG 结构,你可以在图 22-2 中看到弧线路径是自动创建的,并且每个切片都有一个<g>元素。

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

图 22-2。

With Firebug, you can see how the D3 library automatically builds the arc element

此外,有必要给每个切片添加一个指示性标签,以便您可以理解它与哪个国家相关,如清单 22-9 所示。注意arc.centroid()功能。该函数计算圆弧的质心。质心被定义为内半径和外半径以及起始角度和终止角度之间的中点。因此,标签文本完美地出现在每个切片的中间。

清单 22-9。ch22_01a.html

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

...

g.append("path")

.attr("d", arc)

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

g.append("text")

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

return "translate(" + arc.centroid(d) + ")"; })

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

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

});

即使对于饼状图,在顶部和中心位置添加标题也是一个好习惯(见清单 22-10)。

清单 22-10。ch22_01a.html

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

...

g.append("text")

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

return "translate(" + arc.centroid(d) + ")"; })

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

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

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 first pie chart");

});

至于 CSS 类属性,您可以添加清单 22-11 中的定义。

清单 22-11。ch22_01a.html

<style>

body {

font: 16px sans-serif;

}

.arc path {

stroke: #000;

}

</style>

最后,你使用 D3 库得到了你的第一个饼状图,如图 22-3 所示。

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

图 22-3。

A simple pie chart

饼图的一些变化

现在,您将对刚刚创建的基本饼图进行一些更改,展示您可以获得的主题变化的无限可能性:

  • 处理颜色序列;
  • 对饼图中的切片进行排序;
  • 在切片之间添加空间;
  • 仅用轮廓表示切片;
  • 结合以上所有内容。
处理颜色序列

在前面的例子中,我们定义了色阶中的颜色,在前面的例子中我们使用了category10()函数。还有其他已经定义的分类色标:category20()category20b()category20c()。将它们应用到您的饼图中,看看它们如何影响其外观。清单 22-12 显示了一个使用category10()函数的例子。对于其他类别,您只需将此函数替换为其他函数。

清单 22-12。ch22_01b.html

var color = d3.scale.category10();

图 22-4 显示了刻度之间的颜色变化(反映在打印的不同灰度色调中)。category10()和 category20()函数生成具有交替颜色的刻度;相反,类别 20b()和类别 20c()会生成一个颜色渐变缓慢的刻度。

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

图 22-4。

Different color sequences: a) category10, b) category20, c) category20b, d) category20c

对饼图中的扇区进行排序

另一件要注意的事情是,默认情况下,D3 中的饼图经历了隐式排序。因此,如果您没有通过将null传递给sort()函数来显式地发出请求,如清单 22-13 所示。

清单 22-13。ch22_01c.html

var pie = d3.layout.pie()

//.sort(null)

.value(function(d) { return d.income; });

然后,饼图看起来会有所不同,如图 22-5 所示。

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

图 22-5。

A simple pie chart with sorted slices

在饼图中,第一个切片最大,然后其他切片按降序逐渐添加。

在切片之间添加空间

通常,切片显示为在它们之间间隔开,这可以非常容易地实现。您只需要对清单 22-14 所示的path元素的 CSS 样式类进行一些修改。

清单 22-14。ch22_01d.html

.arc path {

stroke: #fff;

stroke-width: 4;

}

图 22-6 显示了当扇区被白色间隙隔开时,饼图如何呈现更令人愉悦的外观。

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

图 22-6。

The slices are separated by a white space

仅用轮廓表示切片

绘制带有切片的饼图要稍微复杂一些,这些切片只有彩色边框,内部是空的。您已经看到了 jqPlot 的类似案例。更改 CSS 样式类,如清单 22-15 所示。

清单 22-15。ch22_01e.html

.arc path {

fill: none;

stroke-width: 6;

}

事实上,这一次您不希望用特定的颜色填充切片,而是希望定义它们的边用特定的颜色着色。所以需要在 SVG 元素的样式定义中用stroke属性替换fill。现在是用指示色着色的线。但是你需要做另一个改变,这个改变有点复杂,很难理解。

您使用每个切片的边界来指定彩色部分,但它们实际上是重叠的。所以,下面的颜色覆盖了前一个颜色的一部分,把所有的切片都连在一起就不那么整齐了。再加个小缺口就更好了。这很容易做到,只需对每个切片进行平移。每个切片都应该在离心方向上偏离中心一小段距离。因此,每个切片的平移是不同的,这里你利用了centroid()函数的功能,它给出了平移的方向(x 和 y 坐标)(见清单 22-16)。

清单 22-16。ch22_01e.html

var g = svg.selectAll(".arc")

.data(pie(data))

.enter().append("g")

.attr("class", "arc")

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

a = arc.centroid(d)[0]/6;

b = arc.centroid(d)[1]/6;

return "translate(" + a +","+b + ")";

})

g.append("path")

.attr("d", arc)

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

图 22-7 展示了这些变化如何影响饼图。

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

图 22-7。

A pie chart with unfilled slices

混合所有这些

但这并没有结束。您可以在这最后两个饼图之间创建一个中间解决方案:获取具有更深颜色边缘的切片,并用更浅的颜色填充它们。定义两条相同但颜色不同的路径就足够了,如清单 22-17 所示。第一种颜色均匀,略显暗淡,而第二种只有彩色边缘,内部为白色。

清单 22-17。ch22_01f.html

var g = svg.selectAll(".arc")

.data(pie(data))

.enter().append("g")

.attr("class", "arc")

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

a = arc.centroid(d)[0]/6;

b = arc.centroid(d)[1]/6;

return "translate(" + a +","+b + ")";

})

g.append("path")

.attr("d", arc)

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

.attr('opacity', 0.5);

g.append("path")

.attr("d", arc)

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

图 22-8 显示了带有两条路径的间隔饼图,这两条路径为切片及其边界着色。

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

图 22-8。

A different way to color the slices in a pie chart

圆环图

就像饼图对于条形图一样,圆环图对于多系列条形图也是如此。事实上,当您有多组值时,您必须用每个系列的饼图来表示它们。如果你使用甜甜圈图,你可以把它们放在一起,并在一个图表中进行比较(见图 22-9 )。

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

图 22-9。

A diagram representing the parallelism between pie charts and bar charts both with one and with multiple series of data

首先编写清单 22-18 中的代码;我们将不提供任何解释,因为它与前面的例子相同。

清单 22-18。ch22_02.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.arc path {

stroke: #000;

}

</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 color = d3.scale.ordinal()

.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);

var radius = Math.min(w, h) / 2;

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(" +(w/2+margin.left)+

"," +(h/2+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("A donut chart");

</script>

</body>

</html>

对于一个多系列数据的例子,您将向文件data_04.csv中添加另一列表示费用的数据,如清单 22-19 所示,并且您将把这个新版本保存为data_06.csv

清单 22-19。data_06.csv

country,income,expense

France,14,10

Russia,22,19

Japan,13,6

South Korea,34,12

Argentina,28,26

您添加了一组新数据。因此,与上一个示例不同,您必须为此系列创建一个新弧线。然后,除了第二条弧线之外,再添加第三条弧线。这个弧不会绘制一个系列的切片,但是您将使用它来循环分布标签。这些标签显示了国家的名称,为图例提供了另一种选择。因此,将半径分成三部分,中间留一个空隙来分隔系列,如清单 22-20 所示。

清单 22-20。ch22_02.html

var arc1 = d3.svg.arc()

.outerRadius(0.4 * radius)

.innerRadius(0.2 * radius);

var arc2 = d3.svg.arc()

.outerRadius(0.7 * radius )

.innerRadius(0.5 * radius );

var arc3 = d3.svg.arc()

.outerRadius(radius)

.innerRadius(0.8 * radius);

您刚刚创建了两个弧来管理这两个系列,因此现在有必要创建两个饼图,一个用于收入值,另一个用于支出值(见清单 22-21)。

清单 22-21。ch22_02.html

var pie = d3.layout.pie()

.sort(null)

.value(function(d) { return d.income; });

var pie2 = d3.layout.pie()

.sort(null)

.value(function(d) { return d.expense; });

使用d3.csv()函数读取文件中的数据,如清单 22-22 所示。您使用forEach()进行通常的数据迭代,将收入和费用解释为数值。

清单 22-22。ch22_02.html

d3.csv("data_06.csv", function(data) {

data.forEach(function(d) {

d.income = +d.income;

d.expense = +d.expense;

});

});

在清单 22-23 中,您创建了path元素,它绘制了两个甜甜圈的不同部分,对应于两个系列。使用函数data(),您将两个饼图布局的数据绑定到两个表示。两个甜甜圈必须遵循相同的颜色顺序。一旦定义了 path 元素,就可以用一个报告相应数值的text元素来连接它。因此,您添加了一些标签,使图表更容易阅读。

清单 22-23。ch22_02.html

var g = svg.selectAll(".arc1")

.data(pie(data))

.enter().append("g")

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

g.append("path")

.attr("d", arc1)

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

g.append("text")

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

return "translate(" + arc1.centroid(d) + ")"; })

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

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

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

var g = svg.selectAll(".arc2")

.data(pie2(data))

.enter().append("g")

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

g.append("path")

.attr("d", arc2)

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

g.append("text")

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

return "translate(" + arc2.centroid(d) + ")"; })

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

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

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

现在,剩下要做的就是添加执行图例功能的外部标签,如清单 22-24 所示。

清单 22-24。ch22_02.html

g.append("text")

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

return "translate(" + arc3.centroid(d) + ")"; })

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

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

这样你就得到了如图 22-10 所示的环形图。

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

图 22-10。

A donut chart

极区图

极区图非常类似于饼图,但它们的不同之处在于每个扇区从圆心延伸的距离,从而可以表示更远的值。每个切片的范围与这个新的附加值成比例(见图 22-11 )。

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

图 22-11。

In a polar area diagram, each slice is characterized by a radius r and an angle

再次考虑文件data_04.csv中的数据,添加一个额外的列来显示相应国家的增长,如清单 22-25 所示。另存为data_07.csv

清单 22-25。data_07.csvl

country,income,growth

France,14,10

Russia,22,19

Japan,13,9

South Korea,34,12

Argentina,28,16

开始编写清单 22-26 中的代码;同样,我们不会解释这一部分,因为它与前面的例子相同。

清单 22-26。ch22_03.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

body {

font: 16px sans-serif;

}

.arc path {

stroke: #000;

}

</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 color = d3.scale.ordinal()

.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);

var radius = Math.min(w, h) / 2;

var pie = d3.layout.pie()

.sort(null)

.value(function(d) { return d.income; });

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(" +(w/2-margin.left)+

"," +(h/2+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("A polar area diagram");

</script>

</body>

</html>

在清单 22-27 中,您用d3.csv()函数读取了data_07.csv文件中的数据,并确保收入和增长率的值被解释为数值。

清单 22-27。ch22_03.html

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

data.forEach(function(d) {

d.income = +d.income;

d.growth = +d.growth;

});

});

与前面的例子不同,这里你不仅定义了一个弧线,而且定义了一个随着被读取数据的变化而变化的弧线;我们称之为arcs,因为outerRadius不再是常数,而是与文件中的增长值成比例。为了做到这一点,你需要应用一个通用的迭代函数,然后弧线必须在d3.csv()函数中声明(见清单 22-28)。

清单 22-28。ch22_03.html

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

data.forEach(function(d) {

d.income = +d.income;

d.growth = +d.growth;

});

arcs = d3.svg.arc()

.innerRadius( 0 )

.outerRadius( function(d,i) { return 8*d.data.growth; });

});

现在,您只需添加 SVG 元素,这些元素绘制带有包含增长和收入值的标签的切片(参见清单 22-29)。报告收入值的标签将绘制在切片内,就在由centroid()函数返回的值处。相反,关于报告生长值的标签,它们将被画在切片的外面。要获得这种效果,您可以使用由centroid()返回的 x 和 y 值,并将它们乘以一个大于 2 的值。你一定记得质心在角度的正中心,在innerRadiusouterRadius的中间。因此,将它们乘以 2,就得到切片外边缘中心的点。如果您将它们乘以一个大于 2 的值,那么您将找到切片外部的 x 和 y 位置,就在您想要绘制具有增长值的标签的位置。

清单 22-29。ch22_03.html

var g = svg.selectAll(".arc")

.data(pie(data))

.enter().append("g")

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

g.append("path")

.attr("d", arcs)

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

g.append("text")

.attr("class","growth")

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

a = arcs.centroid(d)[0]*2.2;

b = arcs.centroid(d)[1]*2.2;

return "translate(" +a+","+b+ ")"; })

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

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

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

g.append("text")

.attr("class","income")

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

return "translate(" +arcs.centroid(d)+ ")"; })

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

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

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

您尚未对饼图执行的一项操作是添加图例。在清单 22-30 中,我们在d3.csv()函数之外定义了一个元素<g>来插入图例表,在函数内部我们定义了所有与国家相关的元素,因为定义它们需要访问文件中的值。

清单 22-30。ch22_03.html

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

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

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

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

...

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

.data(pie(data))

.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", function(d) { return color(d.data.country); });

legend.append("text")

.attr("x", w - 24)

.attr("y", 9)

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

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

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

});

最后,您可以对 CSS 样式类进行一些调整,如清单 22-31 所示。

清单 22-31。ch22_03.html

<style>

body {

font: 16px sans-serif;

}

.arc path {

stroke: #fff;

stroke-width: 4;

}

.arc .income {

font: 12px Arial;

color: #fff;

}

</style>

这里是极区图(见图 22-12 )。

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

图 22-12。

A polar area diagram

摘要

在这一章中,你学习了如何使用 D3 库实现饼状图和圆环图,遵循了与前几章几乎相同的指导方针。此外,在本章的最后,你学习了如何制作极区图,这是一种你以前没有见过的图表,D3 库允许你很容易地实现。

在下一章中,您将实现两种类型的烛台图表,您已经在介绍 jqPlot 库的书的第一部分中讨论过了,只是这次您将使用 D3 库。