JavaScript-图表入门指南-十-

91 阅读18分钟

JavaScript 图表入门指南(十)

原文:Beginning JavaScript Charts

协议:CC BY-NC-SA 4.0

二十五、D3 雷达图

Abstract

本章介绍了一种你还没有读过的图表:雷达图。首先,您将了解它是什么,包括它的基本特性,以及如何使用 D3 库提供的 SVG 元素创建一个。

本章介绍了一种你还没有读过的图表:雷达图。首先,您将了解它是什么,包括它的基本特性,以及如何使用 D3 库提供的 SVG 元素创建一个。

首先,从一个 CSV 文件中读取少量有代表性的数据。然后,参考这些数据,您将看到如何一步一步地实现雷达图的所有组件。在本章的第二部分,您将使用相同的代码从更复杂的文件中读取数据,其中要处理的系列数和数据量都更大。当您需要从头开始表示一种新类型的图表时,这种方法是一种相当常见的做法。您从一个简单但完整的例子开始,然后,一旦您实现了基本的例子,您将使用更复杂和真实的数据来扩展它。

雷达图

雷达图也称为网状图或蜘蛛图,因为它们呈现典型的网状结构(见图 25-1 )。它们是二维图表,使您能够表示三个或更多的定量变量。它们由一系列角度相同的辐条组成,每个辐条代表一个变量。每个轮辐上都有一个点,该点离中心的距离与给定变量的大小成正比。然后,一条线连接每个辐条上报告的点,从而给绘图一个网状外观。如果没有这条连接线,图表看起来会更像一个扫描雷达。

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

图 25-1。

A radar chart looks like a spider web

构建自动缩放轴

复制下面的数据,并将其保存在一个文件中,名为data_11.csv(见清单 25-1)。

清单 25-1。data_11.csv

section,set1,set2,

A,1,6,

B,2,7,

C,3,8,

D,4,9,

E,5,8,

F,4,7,

G,3,6,

H,2,5,

在清单 25-2 中,您定义了绘图区域和边距。然后用category10()功能创建一个颜色序列。

清单 25-2。ch25_01.html

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.category20();

在刚刚定义的绘图区域中,您还必须定义一个可以容纳圆形的特定区域,在本例中是雷达图。一旦定义了这个区域,就像笛卡尔轴一样定义了半径。事实上,雷达图上的每个辐条都被认为是一个轴,在这个轴上放置一个变量。因此,在半径上定义一个线性标度,如清单 25-3 所示。

清单 25-3。ch25_01.html

var circleConstraint = d3.min([h, w]);

var radius = d3.scale.linear()

.range([0, (circleConstraint / 2)]);

您需要找到绘图区域的中心。这是雷达图的中心,也是所有辐条的辐射点(见清单 25-4)。

清单 25-4。ch25_01.html

var centerXPos = w / 2 + margin.left;

var centerYPos = h / 2 + margin.top;

开始绘制根元素<svg>,如清单 25-5 所示。

清单 25-5。ch25_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(" + centerXPos + ", " + centerYPos + ")");

现在,如清单 25-6 所示,用d3.csv()函数读取文件内容。您需要验证读取值set1set2是否被解释为数值。您还想知道所有这些值中的最大值,以便定义一个根据其值扩展的刻度。

清单 25-6。ch25_01.html

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

var maxValue = 0;

data.forEach(function(d) {

d.set1 = +d.set1;

d.set2 = +d.set2;

if(d.set1 > maxValue)

maxValue = d.set1;

if(d.set2 > maxValue)

maxValue = d.set2;

});

});

知道输入数据的最大值后,将满量程值设置为该最大值乘以 1.5。在这种情况下,您必须手动定义轴上的记号,而不是使用自动生成的记号。事实上,这些扁虱是球形的,因此具有非常独特的特征。此示例将半径轴的范围分为五个刻度。一旦定义了记号的值,就可以给半径轴分配一个域(见清单 25-7)。

清单 25-7。ch25_01.html

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

...

data.forEach(function(d) {

...

});

var topValue = 1.5 * maxValue;

var ticks = [];

for(i = 0; i < 5;i += 1){

ticks[i] = topValue * i / 5;

}

radius.domain([0,topValue]);

});

现在你已经有了所有的数值,我们可以嵌入一些<svg>元素来设计一个雷达网格,它的形状和值会根据输入的数据而变化(见清单 25-8)。

清单 25-8。ch25_01.html

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

...

radius.domain([0,topValue]);

var circleAxes = svg.selectAll(".circle-ticks")

.data(ticks)

.enter().append("g")

.attr("class", "circle-ticks");

circleAxes.append("svg:circle")

.attr("r", function(d) {return radius(d);})

.attr("class", "circle")

.style("stroke", "#CCC")

.style("fill", "none");

circleAxes.append("svg:text")

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

.attr("dy", function(d) {return radius(d)})

.text(String);

});

您已经创建了一个名为circle-ticks的五个<g>标签的结构,如图 25-2 所示,每个标签包含一个<circle>元素(绘制网格)和一个<text>元素(显示相应的数值)。

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

图 25-2。

FireBug shows how the circle-ticks are structured

所有这些代码生成了如图 25-3 所示的圆形网格。

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

图 25-3。

The circular grid of a radar chart

如您所见,分笔成交点上报告的值将根据数据中包含的最大值而变化。

现在是画轮辐的时候了,和data_11.csv文件中的线条一样多的射线。这些行中的每一行都对应一个变量,变量的名字被输入到文件的第一列中(见清单 25-9)。

清单 25-9。ch25_01.html

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

...

circleAxes.append("svg:text")

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

.attr("dy", function(d) {return radius(d)})

.text(String);

lineAxes = svg.selectAll('.line-ticks')

.data(data)

.enter().append('svg:g')

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

return "rotate(" + ((i / data.length * 360) - 90) +

")translate(" + radius(topValue) + ")";

})

.attr("class", "line-ticks");

lineAxes.append('svg:line')

. attr("x2", -1 * radius(topValue))

.style("stroke", "#CCC")

.style("fill", "none");

lineAxes.append('svg:text')

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

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

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

return "rotate("+(90 - (i * 360 / data.length)) + ")";

});

});

现在图表中显示了辐条,如图 25-4 所示。

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

图 25-4。

The radial axes of a radar chart

向雷达图添加数据

现在是时候考虑文件中的数字列了。每一列都可以被视为一个系列,每个系列都必须分配一种颜色。您可以通过从文件中取出标题并删除第一列来定义系列。然后你可以根据系列的顺序创建颜色域,然后定义绘制它们的线条(见清单 25-10)。

清单 25-10。ch25_01.html

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

...

lineAxes.append('svg:text')

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

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

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

return "rotate("+(90-(i*360/data.length))+")";

});

var series = d3.keys(data[0])

.filter(function(key) { return key !== "section"; })

.filter(function(key) { return key !== ""; });

color.domain(series);

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

return (data.concat(data[0])).map(function(d){

return +d[name];

});

});

});

这是series数组的内容:

[ "set1", "set2" ]

这是lines数组的内容:

[[1,2,3,...],[6,7,8,...]]

这些线将帮助您创建相应的路径元素,并使您能够在雷达图上绘制系列的趋势。每个序列都将通过不同轮辐中的假设值(见清单 25-11)。

清单 25-11。ch25_01.html

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

...

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

return (data.concat(data[0])).map(function(d){

return +d[name];

});

});

var sets = svg.selectAll(".series")

.data(series)

.enter().append("g")

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

sets.append('svg:path')

.data(lines)

.attr("class", "line")

.attr("d", d3.svg.line.radial()

.radius(function (d) {

return radius(d);

})

.angle(function (d, i) {

if (i == data.length) {

i = 0;

} //close the line

return (i / data.length) * 2 * Math.PI;

}))

.data(series)

.style("stroke-width", 3)

.style("fill","none")

.style("stroke", function(d,i){

return color(i);

});

});

您还可以添加一个显示系列名称的图例(实际上是列的标题)和一个标题放在绘图区域的顶部,如清单 25-12 所示。

清单 25-12。ch25_01.html

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

...

.data(series)

.style("stroke-width", 3)

.style("fill","none")

.style("stroke", function(d,i){

return color(i);

});

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

.data(series)

.enter().append("g")

.attr("class", "legend")

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

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

});

legend.append("rect")

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

.attr("y", h/2 - 60)

.attr("width", 18)

.attr("height", 18)

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

legend.append("text")

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

.attr("y", h/2 - 60)

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

.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 Radar Chart");

最后但同样重要的是,您可以添加一个 CSS 样式类来控制文本样式,如清单 25-13 所示。

清单 25-13。ch25_01.html

<style>

body {

font: 16px sans-serif;

}

</style>

图 25-5 显示了生成的雷达图。

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

图 25-5。

A radar chart with two series

改善你的雷达图

如果您按照前面的示例进行操作,那么向雷达图添加更多的列和行应该不会有任何问题。打开最后一个输入数据文件,名为data_11.csv,再添加两列和两行。将文件另存为data_12.csv,如清单 25-14 所示。

清单 25-14。data_12.csv

section,set1,set2,set3,set4,

A,1,6,2,10,

B,2,7,2,14,

C,3,8,1,10,

D,4,9,4,1,

E,5,8,7,2,

F,4,7,11,1,

G,3,6,14,2,

H,2,5,2,1,

I,3,4,5,2,

L,1,5,1,2,

现在您必须在d3.csv()函数中用data12.csv文件替换对data11.csv文件的调用,如清单 25-15 所示。

清单 25-15。ch25_02.html

d3.csv("``data_12.csv

...});

图 25-6 显示了结果。

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

图 25-6。

A radar chart with four series

哇,成功了!准备好添加另一个特性了吗?到目前为止,您已经描绘了一条穿过各种辐条的线,循环返回到起点;趋势现在描述了一个特定的领域。您通常会对雷达图中由不同线条分隔的区域比对线条本身更感兴趣。如果你想对你的雷达图做这个小小的转换以显示区域,你只需要再增加一条路径,如清单 25-16 所示。这个路径实际上与已经存在的路径相同,只是这个新路径没有绘制代表系列的线条,而是对内部封闭的区域进行了着色。在本例中,您将使用相应线条的颜色,但增加一点透明度,以免覆盖底层系列。

清单 25-16。ch25_02.html

d3.csv("``data_12.csv

...

var sets = svg.selectAll(".series")

.data(series)

.enter().append("g")

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

sets.append('svg:path')

.data(lines)

.attr("class", "line")

.attr("d", d3.svg.line.radial()

.radius(function (d) {

return radius(d);

})

.angle(function (d, i) {

if (i == data.length) {

i = 0;

}

return (i / data.length) * 2 * Math.PI;

}))

.data(series)

.style("stroke-width", 3)

.style("opacity", 0.4)

.style("fill",function(d,i){

return color(i);

})

.style("stroke", function(d,i){

return color(i);

}) ;

sets.append('svg:path')

.data(lines)

.attr("class", "line")

.attr("d", d3.svg.line.radial()

...

});

正如你在图 25-7 中看到的,你现在有了一个半透明区域的雷达图。

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

图 25-7。

A radar chart with color filled areas

摘要

本章解释了如何实现雷达图。这种类型的图表用 jqPlot 是不可行的,所以这一章是有用的,部分是为了突出 D3 库的潜力。它展示了一个示例,帮助您理解如何开发不同于最常见类型的其他图表。

下一章也是最后一章通过考虑两个不同的案例来结束这本书。这些案例旨在以简化的方式提出开发人员在处理真实数据时必须面对的典型情况。在第一个例子中,您将看到如何使用 D3 来表示实时生成或获取的数据。您将创建一个不断更新的图表,始终显示当前情况。在第二个例子中,您将使用 D3 库来读取数据库中包含的数据。

二十六、使用 D3 处理实时数据

Abstract

你已经看到了如何用 jqPlot 处理实时图表,在这一章中,你将使用 D3 库实现同样的例子。实际上,您将创建一个折线图,显示模拟外部数据源的函数所生成的实时值。数据会不断生成,因此折线图也会相应变化,始终显示最新情况。

你已经看到了如何用 jqPlot 处理实时图表,在这一章中,你将使用 D3 库实现同样的例子。实际上,您将创建一个折线图,显示模拟外部数据源的函数所生成的实时值。数据会不断生成,因此折线图也会相应变化,始终显示最新情况。

在本章的第二部分,你将制作一个稍微复杂一点的图表。这一次,您将使用一个例子,其中的数据源是一个真实的数据库。首先,您将实现一个折线图,它将读取外部文件中包含的数据。稍后,您将学习如何使用该示例来读取相同的数据,但这次是从数据库的表中读取。

实时图表

您有一个模拟函数的数据源,该函数返回变量性能的随机变化。这些值存储在一个具有缓冲区功能的数组中,其中只包含最近的 10 个值。对于生成或获取的每个输入值,最旧的值将被新值替换。此数组中包含的数据显示为每 3 秒更新一次的折线图。点击一个按钮就可以激活一切。

让我们开始设置表示折线图的基础(查看用 D3 库开发折线图,见第二十章)。首先,你写一个 HTML 结构,你将在这个结构上构建你的图表,如清单 26-1 所示。

清单 26-1。ch26_01.html

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

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

<style>

//add the CSS styles here

</style>

<body>

<script type="text/javascript">

// add the JavaScript code here

</script>

</body>

</html>

现在,从输入数据的管理开始,您开始定义在代码编写过程中对您有帮助的变量。如前所述,您将要在图表上表示的数据来自一个函数,该函数从一个初始值开始产生随机变量,可以是正的,也可以是负的。你决定从 10 点开始。

因此,从这个值开始,你从随机函数接收一个要在一个数组中收集的值序列,你将称之为data(见清单 26-2)。现在,你只在起始值(10)内给它赋值。给定这个数组,要实时接收值,您需要设置一个最大限制,在本例中是 11 个元素(0–10)。填充后,您将把数组作为一个队列来管理,其中最旧的项将被删除,以便为新项腾出空间。

清单 26-2。ch26_01.html

<script type="text/javascript">

var data = [10];

w = 400;

h = 300;

margin_x = 32;

margin_y = 20;

ymax = 20;

ymin = 0;

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

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

</script>

包含在data数组中的值将沿着 y 轴表示,因此有必要建立刻度标签必须覆盖的值的范围。例如,从值 10 开始,您可以决定此范围涵盖从 0 到 20 的值(稍后,您将确保刻度对应于 5 的倍数范围,即 0、5、10、15 和 20)。因为要显示的值是随机生成的,所以它们将逐渐假定值甚至大于 20,如果是这样,您将看到图表顶部边缘的线条消失了。怎么办?

因为主要目标是创建一个在获取新数据后自动重绘的图表,所以您将确保即使是刻度标签也根据包含在data数组中的值所覆盖的范围进行调整。为了实现这一点,您需要用ymaxymin变量定义y范围,用x范围覆盖静态范围[0–10]。

wh变量(宽度和高度)定义了您将在其上绘制折线图的绘图区域的大小,而margin_xmargin_y允许您调整边距。

现在,让我们创建代表图表各个部分的标量矢量图形(SVG)元素。首先创建<svg>根,然后定义 x 和 y 轴,如清单 26-3 所示。

清单 26-3。ch26_01.html

<script type="text/javascript">

...

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

x = d3.scale.linear().domain([0, 10]).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 + ")");

// 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(25));

</script>

接下来,在两个轴上添加记号和相应的标签,然后添加栅格。最后,用line()函数在图表上画线(见清单 26-4)。

清单 26-4。ch26_01.html

<script type="text/javascript">

...

g.append("svg:line")

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

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

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

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

//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(25));

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

var line = d3.svg.line()

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

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

</script>

为了给你的图表一个令人愉快的外观,定义层叠样式表(CSS)样式也是必要的,如清单 26-5 所示。

清单 26-5。ch26_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>

现在,您在图表上方添加一个按钮,确保当用户点击它时updateData()功能被激活,如清单 26-6 所示。

清单 26-6。ch26_01.html

< body>

<div id="option">

<input name="updateButton"

type="button"

value="Update"

onclick="updateData()" />

</div>

然后,你实现getRandomInt()函数,它产生一个介于最小值和最大值之间的随机整数值(见清单 26-7)。

清单 26-7。ch26_01.html

function getRandomInt (min, max) {

return Math.floor(Math.random() * (max - min + 1)) + min;

};

清单 26-8 显示了updateData()函数,其中由getRandomInt()函数生成的值被添加到数组的最近值中,以模拟趋势的变化。这个新值存储在数组中,而最旧的值被删除;因此,数组的大小总是保持不变。

清单 26-8。ch26_01.html

function updateData() {

var last = data[data.length-1];

if(data.length > 10){

data.shift();

}

var newlast = last + getRandomInt(-3,3);

if(newlast < 0)

newlast = 0;

data.push(newlast);

};

如果由getRandomInt()函数返回的新值大于或小于 y 轴上表示的范围,您将看到数据行延伸到图表的边缘。为了防止这种情况发生,你必须通过改变yminymax变量来改变 y 轴上的间隔,并用这些新值更新y范围,如清单 26-9 所示。

清单 26-9。ch26_01.html

function updateData() {

...

if(newlast < 0)

newlast = 0;

data.push(newlast);

if(newlast > ymax){

ymin = ymin + (newlast - ymax);

ymax = newlast;

y = d3.scale.linear().domain([ymin, ymax])

.range([0 + margin_y, h - margin_y]);

}

if(newlast < ymin){

ymax = ymax - (ymin - newlast);

ymin = newlast;

y = d3.scale.linear().domain([ymin, ymax])

.range([0 + margin_y, h - margin_y]);

}

};

因为获得的新数据必须重新绘制,所以您需要删除无效的 SVG 元素并用新的元素替换它们。让我们对刻度标签和数据行都这样做(见清单 26-10)。最后,需要定时重复刷新图表。因此,使用requestAnimFrame()函数,您可以重复执行UpdateData()函数的内容。

清单 26-10。ch26_01.html

function updateData() {

...

if(newlast < ymin){

ymax = ymax - (ymin - newlast);

ymin = newlast;

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

}

var svg = d3.select("body").transition();

g.selectAll(".yLabel").remove();

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

g.selectAll(".line").remove();

g.append("svg:path")

.attr("class","line")

.attr("d", line(data));

window.requestAnimFrame = (function(){

return window.requestAnimationFrame ||

window.webkitRequestAnimationFrame   ||

window.mozRequestAnimationFrame      ||

function( callback ){

window.setTimeout(callback, 1000);

};

})();

requestAnimFrame(setTimeout(updateData,3000));

render();

};

现在,当点击 Update 按钮时,从左边开始有一条线画出由生成随机变量的函数获得的值。一旦该线到达图表的右端,它将更新每次采集,仅显示最后十个值(见图 26-1 ) .

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

图 26-1。

A real-time chart with a start button

使用 PHP 从 MySQL 表中提取数据

最后,是时候使用数据库中包含的数据了,这种场景更符合您的日常需求。您选择 MySQL 作为数据库,并使用超文本预处理器(PHP)语言查询数据库,获得 JavaScript 对象表示法(JSON)格式的数据,以便 D3 可以读取。你会发现,一旦用 D3 构建了一个图表,过渡到这个阶段就很容易了。

下面这个例子不是为了解释 PHP 语言或者其他任何语言的使用,而是为了说明一个典型的真实案例。这个例子显示了将你所学的知识与其他编程语言结合起来是多么简单。通常,像 Java 和 PHP 这样的语言提供了一个很好的接口来从它们的来源(在这个例子中是数据库)收集和准备数据。

从 TSV 的档案开始

为了更清楚地理解你已经知道的东西与 PHP 和数据库接口之间的转换,让我们从一个你应该熟悉的案例开始(见清单 26-11)。首先,用这些系列数据编写一个制表符分隔的值(TSV)文件,并将它们保存为data_13.tsv

清单 26-11。data_13.tsv

day           income  expense

2012-02-12    52     40

2012-02-27    56     35

2012-03-02    31     45

2012-03-14    33     44

2012-03-30    44     54

2012-04-07    50     34

2012-04-18    65     36

2012-05-02    56     40

2012-05-19    41     56

2012-05-28    45     32

2012-06-03    54     44

2012-06-18    43     46

2012-06-29    39     52

Note

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

实际上,正如所暗示的,你已经看到了这些数据,尽管形式略有不同;这些是data_03.tsv文件中的相同数据(参见第二十章中的清单 20-60)。您将列date更改为day,并修改了日期的格式。现在,您必须添加清单 26-12 中的 JavaScript 代码,这将允许您将这些数据表示为多系列折线图。(该代码与第二十章中的“差异折线图”一节中使用的代码非常相似;关于清单 26-12 内容的解释和细节,请参阅该部分。)

清单 26-12。ch26_02.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: darkgreen;

stroke-width: 2.5px;

}

.line2 {

fill: none;

stroke: darkred;

stroke-width: 2.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("%Y-%m-%d").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.area()

.interpolate("basis")

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

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

var line2 = d3.svg.area()

.interpolate("basis")

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

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

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

data.forEach(function(d) {

d.day = parseDate(d.day);

d.income = +d.income;

d.expense = +d.expense;

});

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

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

svg.datum(data);

svg.append("path")

.attr("class", "line")

.attr("d", line);

svg.append("path")

.attr("class", "line2")

.attr("d", line2);

});

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 Multiseries Line Chart");

</script>

</body>

</html>

有了这段代码,你就得到图 26-2 中的图表。

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

图 26-2。

A multiseries chart reading data from a TSV file

转到真正的案例

现在,让我们转到实际情况,您将处理数据库中的表。对于数据源,您在 MySQL 的测试数据库中选择一个名为 sales 的表。在用这个名称创建了一个表之后,可以用执行 SQL 序列的数据填充它(见清单 26-13)。

清单 26-13。销售. sql

insert into sales

values ('2012-02-12', 52, 40);

insert into sales

values ('2012-02-27', 56, 35);

insert into sales

values ('2012-03-02', 31, 45);

insert into sales

values ('2012-03-14', 33, 44);

insert into sales

values ('2012-03-30', 44, 54);

insert into sales

values ('2012-04-07', 50, 34);

insert into sales

values ('2012-04-18', 65, 36);

insert into sales

values ('2012-05-02', 56, 40);

insert into sales

values ('2012-05-19', 41, 56);

insert into sales

values ('2012-05-28', 45, 32);

insert into sales

values ('2012-06-03', 54, 44);

insert into sales

values ('2012-06-18', 43, 46);

insert into sales

values ('2012-06-29', 39, 52);

最好是,您应该在一个单独的文件中编写 PHP 脚本,并将其保存为myPHP.php。这个文件的内容如清单 26-14 所示。

清单 26-14 . myPHP.php

<?php

$username = "dbuser";

$password = "dbuser";

$host = "localhost";

$database = "test";

$server = mysql_connect($host, $username, $password);

$connection = mysql_select_db($database, $server);

$myquery = "SELECT * FROM sales";

$query = mysql_query($myquery);

if ( ! $myquery ) {

echo mysql_error();

die;

}

$data = array();

for ($x = 0; $x < mysql_num_rows($query); $x++) {

$data[] = mysql_fetch_assoc($query);

}

echo json_encode($data);

mysql_close($server);

?>

通常,一个 PHP 脚本可以通过它在特殊的开始和结束处理指令:<?php?>中的封装来识别。每当我们需要连接到数据库时,通常都会用到这段简短但功能强大且用途广泛的代码片段。让我们浏览一下,看看它做了什么。

在本例中,dbuser被选为用户,dbuser被选为密码,但是这些值将取决于您想要连接的数据库。这同样适用于数据库和主机名值。因此,为了连接到一个数据库,你必须首先定义一组识别变量,如清单 26-15 所示。

清单 26-15 . myPHP.php

$username = "homedbuser";

$password = "homedbuser";

$host = "localhost";

$database="homedb";

一旦定义了它们,PHP 就提供了一组已经实现的函数,在这些函数中,您只需将这些变量作为参数传递,就可以与数据库建立连接。在这个例子中,您需要调用mysql_connect()myqsl_select_db()函数来创建一个与数据库的连接,而不需要定义任何其他东西(参见清单 26-16)。

清单 26-16 . myPHP.php

$server = mysql_connect($host, $username, $password);

$connection = mysql_select_db($database, $server);

即使对于输入 SQL 查询,PHP 也被证明是一个真正实用的工具。清单 26-17 是一个非常简单的例子,展示了如何使用 SQL 查询从数据库中检索数据。如果您不熟悉 SQL 语言,查询是针对特定数据库的声明性语句,目的是获取数据库中包含的所需数据。您可以很容易地识别一个查询,因为它由一个SELECT语句和一个 FROM 语句组成,并且几乎总是在末尾有一个WHERE语句。

Note

如果你没有 SQL 语言的经验,并且想在不安装数据库和其他任何东西的情况下做一些练习,我建议你访问 w3schools 网站的这个网页: www.w3schools.com/sql 。在本文中,您将找到关于这些命令的完整文档,其中包含许多示例,甚至能够通过嵌入 SQL 测试查询来查询该站点提供的数据库证据(参见“亲自尝试”一节)。

在这个简单的例子中,SELECT语句后面跟有'*',这意味着您想要接收在FROM语句(在这个例子中是sales)中指定的表中包含的所有列中的数据。

清单 26-17 . myPHP.php

$myquery = "SELECT * FROM sales";

$query = mysql_query($myquery);

一旦你做了一个查询,你需要检查它是否成功,如果出现错误就处理它(见清单 26-18)。

清单 26-18 . myPHP.php

if ( ! $query ) {

echo mysql_error();

die;

}

如果查询成功,那么您需要处理查询返回的数据。你把这些值放在一个名为$data的数组中,如清单 26-19 所示。这部分非常类似于 D3 的csv()tsv()函数,只是它不是从文件中逐行读取,而是从数据库中检索的表中读取。mysql_num_rows()函数给出了表中的行数,类似于在for()循环中使用的 JavaScript 的 length()函数。mysql_fetch_assoc()函数将从查询中检索到的数据逐行分配给数据数组。

清单 26-19 . myPHP.php

$data = array();

for ($x = 0; $x < mysql_num_rows($query); $x++) {

$data[] = mysql_fetch_assoc($query);

}

echo json_encode($data);

该脚本的关键是对 PHP json_encode()方法的调用,该方法将数据格式转换成 JSON,然后使用 echo 返回数据,D3 将解析这些数据。最后,您必须关闭与服务器的连接,如清单 26-20 所示。

清单 26-20 . myPHP.php

mysql_close($server);

现在,您回到 JavaScript 代码,只修改了一行(是的,只有一行!)(参见清单 26-21)。用json()函数替换tsv()函数,直接传递 PHP 文件作为参数。

清单 26-21。ch26_02b.html

d3.json("myPHP.php", function(error, data) {

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

data.forEach(function(d) {

d.day = parseDate(d.day);

d.income = +d.income;

d.expense = +d.expense;

});

最终,你会得到同样的图表(见图 26-3 )。

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

图 26-3。

A multiseries chart obtaining data directly from a database

摘要

这最后一章通过考虑两种不同的情况而结束。在第一个例子中,您看到了如何创建一个 web 页面,在这个页面中可以表示您实时生成或获取的数据。在第二个例子中,您学习了如何使用 D3 库来读取数据库中包含的数据。

结论

有了这一章,你就到了这本书的结尾。我必须说,尽管涵盖了大量的主题,但我还想补充许多其他主题。我希望这本书能让你更好地理解数据可视化的世界,尤其是图表。我还希望这本书为您提供了良好的数据可视化基础知识,并证明它对您处理图表的所有场合都是有价值的帮助。

二十七、书中例子的指南

Abstract

本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的 PC 上创建一个开发环境的指南,该环境将允许您开发、运行和修复书中给出的示例。

本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的 PC 上创建一个开发环境的指南,该环境将允许您开发、运行和修复书中给出的示例。

安装 Web 服务器

如今,在互联网上,您可以很容易地找到免费的软件包,其中包含了为您的所有示例以及与 web 世界相关的所有内容建立测试环境所需的一切。

这些软件包最大限度地减少了需要安装的程序数量。更重要的是,它们可以通过一次安装获得。这些包通常由一个 Apache HTTP 服务器组成;一个 MySQL 数据库;以及编程语言 PHP、Perl 和 Python 的解释器。最完整的包是 XAMPP(可以在 Apache Friends 网站[www.apachefriends.org/en/index.html]]下载)。XAMPP 是完全免费的,它的主要特点是它是一个跨平台的软件包(Windows、Linux、Solaris、MacOS)。此外,XAMPP 还包括一个 Tomcat 应用服务器(用于编程语言 Java)和一个 FileZilla FTP 服务器(用于文件传输)。其他解决方案是特定于平台的,正如其名称的首字母所示:

  • WAMP (Windows)
  • MAMP (MacOS)
  • LAMP (Linux)
  • SAMP (Solaris)
  • FAMP

事实上,XAMPP 是一个缩写;其字母代表以下术语:

  • X,为操作系统
  • A,对于 Apache,web 服务器
  • M,对于 MySQL,数据库管理系统
  • P,PHP、Perl 或 Python 的编程语言

因此,选择最适合您的平台的 web 服务器解决方案,并将其安装在您的 PC 上。

安装 Aptana Studio IDE

一旦安装了 Web 服务器,就需要安装一个集成开发环境(IDE ),这是开发 JavaScript 代码所需要的。在本附录中,您将安装 Aptana Studio 作为您的开发环境。

访问 Aptana 站点( www.aptana.com ),并单击 Aptana Studio 3 软件的产品选项卡(在撰写本文时,最新版本是 3.4.2)。下载单机版(已经集成了 Eclipse IDE):Aptana_Studio_3_Setup_3.4.2.exe

下载完成后,启动可执行文件来安装 Aptana Studio IDE。在安装结束时,在启动应用时,您应该看到工作台打开,如图 27-1 所示。

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

图 27-1。

The Aptana Studio IDE workbench

在 Aptana Studio 的安装过程中,软件会检测各种浏览器和安装的 web 服务器,并相应地进行自我配置。

设置 Aptana Studio 工作空间

在开始开发书中的例子之前,您必须创建一个工作空间。首先,您应该在 Aptana Studio 上设置工作区,Web 服务器文档根目录就在这里。

这些是 XAMPP 的典型道路:

  • 视窗:C:\xampp\htdocs
  • Linux:??]
  • MAC OS:??]

而 WAMP 的情况是这样的:

  • C:\WAMP\www

因此,选择文件➤切换工作区➤其他。。。从菜单上。然后,在字段中插入 web 服务器文档根目录的路径,如图 27-2 所示。

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

图 27-2。

Setting the workspace on the document root

创建项目

创建工作空间的下一步是在 Aptana Studio 中创建一个项目:

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

图 27-3。

Creating a default project Select New ➤ Web Project from the menu.   A window such as that shown in Figure 27-3 appears. Select Default Project, and click Next.   Insert “charts” as the name of the project. This will be the directory in the workspace in which you will write all the example files described in the book, using Aptana Studio.  

完成工作空间

一旦您设置了 Aptana Studio 工作区并创建了一个项目,您就完成了工作区。

让我们打开文档根目录并创建一个新目录,命名为src。现在,wyou 将在整本书中使用的工作空间由两个目录组成:

  • src
  • charts

src目录应该包含所有与库相关的文件。

charts目录应该包含所有与书中例子相关的 HTML、图片和层叠样式表(CSS)文件(实际上是一个项目)。每个示例文件都应该在这个目录中创建(如果您喜欢以不同的方式做事情,这很好,但是为了包含库文件和图像,注意 HTML 页面中不同的路径引用是很重要的)。

Note

这本书附带的源代码(可以从 Apress 网站[ www.apress.com ]的源代码/下载区获得)实际上已经打包在一个工作区中了。有了它,你会发现 charts 项目的两个版本:内容交付网络(CDN)和本地。charts_CDN目录包含所有引用从 CDN 服务远程分发的库的例子。charts_local目录提供了所有引用在src目录中找到的库的例子。

用库填充 src 目录

如果你选择了通过引用本地的库来开发 HTML 页面,那么就有必要下载它们所有的文件。这些文件将被收集在src目录中。这是一个很好的方法,因为您可以开发几个使用相同库的项目,而不必为每个项目复制它们。

本附录中列出的版本是用于实现书中示例的版本。如果您安装其他版本,可能会出现不兼容的问题,或者您可能会观察到与描述不同的行为。

jqPlot 库版本 1.0.8(包括 jQuery 库版本 1.9.1)

Visit the jqPlot web site ( https://bitbucket.org/cleonello/jqplot/downloads/ ), and download the compressed file (.zip, .tar.gz or tar.bz2) for the library: jquery.jqplot.1.0.8r1250.   Extract all content. You should get a directory named dist, containing the following subdirectories and files:

  • doc
  • examples
  • plugins
  • 一系列文件(jquery.min.jsjquery.jqplot.min.js等)

  Copy the set of files and the plugins directory, and place in src.  

jquery UI 库版本 1.10.3,带有平滑主题

Visit the JQuery user interface library (jQuery UI) site ( http://jqueryui.com/themeroller/ ), and download the library from ThemeRoller, with the smoothness theme: jquery-ui-1.10.3.custom.zip.   Extract all content. You should get a directory named jquery-ui-1.10.3.custom, with the following directories inside:

  • css
  • js
  • development-bundle

  Copy the css and js directories, and place in src.  

D3 库版本 3

Visit the D3 site ( http://d3js.org ), and download the library: d3.v3.zip.   Extract all content directly, and place in the src directory. Now, you should have two new files in the src directory:

  • d3.v3.js
  • d3.v3.min.js

Highcharts 库版本 3.0.5

Visit the Highcharts site ( www.highcharts.com ), and download the library: Highcharts-3.0.5.zip.   Extract all content. You get a directory with several directories inside.   Copy only the js directory, and place in src.  

这样你就获得了src目录,其中应该包含如图 27-4 所示的子目录和文件。

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

图 27-4。

The files and subdirectories contained in the src directory Note

按照惯例,您正在开发charts目录中的示例。如果您想这样做,当您将其他文件包含在网页中时,您需要考虑新的路径。

如果您正在开发charts目录中的 HTML 页面,您需要使用以下代码:

<script type="text/javascript" src="../src/jquery.min.js"></script>

相反,如果您喜欢直接开发它,在文档根目录中,您可以使用:

<script type="text/javascript" src="src/jquery.min.js"></script>

简而言之,根据您实现的页面,考虑您要包含的文件的路径是很重要的。

运行示例

在工作区中创建或复制 HTML 文件后,要在 Aptana Studio IDE 中运行该文件,请从菜单中选择运行➤运行,或单击工具栏上的运行按钮(参见图 27-5 )。

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

图 27-5。

The Run button from the toolbar

您的默认浏览器将立即打开,并加载选定的 HTML 页面。

查看运行配置(参见图 27-6 ),选择运行配置。。。从运行图标的上下文菜单中。例如,让我们将http://localhost/设置为您的基本 URL 为此,请选择“追加项目名称”选项,如图所示。然后,单击“应用”按钮确认您的设置。

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

图 27-6。

The run configuration for each browser must be set correctly

现在,您已经具备了轻松完成书中所有示例所需的一切。

一旦您对 Aptana IDE 有了一定的了解,您会发现它是一个开发许多其他项目的优秀环境,无论是用 JavaScript 还是用其他编程语言(例如 PHP)。

现在,玩得开心点!

摘要

本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的电脑上创建开发环境的指南。使用这些应用的选择不是强制性的,许多其他解决方案也是可能的;互联网上有许多可用于执行类似操作的应用。但是,如果您希望实现并快速测试书中描述的示例,这个环境将是一个不错的选择。

二十八、jqPlot 插件

Abstract

本附录显示了 jqPlot 发行版中可用插件的完整列表(见表 B-1)。并非所有这些插件都在本书中讨论过;更多信息请访问 jqPlot 网站( www.jqplot.com )。

本附录显示了 jqPlot 发行版中可用插件的完整列表(见表 B-1 )。并非所有这些插件都在本书中讨论过;更多信息请访问 jqPlot 网站( www.jqplot.com )。

表 B-1。

Available Plug-ins in the jqPlot Distribution (version 1.0.8)

| 名字 | 类型 | 描述 | | --- | --- | --- | | $ jqlot,barrenderer | 渲染器 | 画一个条形图。 | | $.jqplot.BezierCurveRenderer | 渲染器 | 将线条绘制成堆叠的贝塞尔曲线。 | | $ . jqplot . blockrenderer | 渲染器 | 画一个 xy 方块图。块图中的数据点显示为彩色方块,内部带有文本标签。 | | $ . jqplot . bubblerenderer | 渲染器 | 画一个气泡图。气泡图的数据点显示为彩色圆圈,内部带有可选的文本标签。 | | $ . jqplot . canvasaxilabelrenderer | 渲染器 | 绘制轴标签,用一个`canvas`元素来支持高级特性,比如旋转文本。这个渲染器使用一个单独的渲染引擎在画布上绘制文本。 | | $ . jqplot . canvasaxistickrenderer | 渲染器 | 用一个`canvas`元素绘制轴记号,以支持高级功能,如旋转文本。这个渲染器使用一个单独的渲染引擎在画布上绘制文本。 | | $ .jqplot.canvasoverlay 重叠 | 插件 | 在图表上画线。 | | $.jqplot.CanvasTextRenderer | 渲染器 | `canvastext.js`插件的修改版,由 Jim Studt ( [`http://jim.studt.net/canvastext/`](http://jim.studt.net/canvastext/) )编写。 | | $ . jqplot . categoryaxisrenderer | 渲染器 | 呈现类别样式轴,系列的 y 数据值之间的像素间距相等。 | | $.jqplot.ciParser | 插件 | 将自定义 JavaScript 对象符号(JSON)数据对象转换为 jqPlot 数据格式的函数。 | | $ . jqplot .游标 | 插件 | 代表光标的类,如图上所示。 | | jqplot . dateaxisrenderer | 渲染器 | 将轴呈现为一系列日期值。 | | $ . jqplot . donutrenderer | 渲染器 | 画一个圆环图;x 值(如果存在)用作切片标签,y 值给出切片大小。 | | $.jqplot.Dragable | 插件 | 制作用户可以拖动的标绘点。 | | $ jqp lot .增强传奇人物 | 渲染器 | 绘制具有高级功能的图例。 | | $.jqplot.漏斗渲染器 | 渲染器 | 画一个漏斗图;x 值(如果有)用作标注,y 值给出面积大小。漏斗图仅绘制单一系列。 | | $ jqlot,高个子 | 插件 | 当鼠标悬停在数据点上时高亮显示它们。 | | jqplot . json 2 | 插件 | 创建一个包含两个方法的 JSON 对象:`stringify()`和`parse()`。 | | $ . jqplot . logaxisrenderer | 渲染器 | 渲染对数轴。 | | $ . jqp lot . meko ximatics 渲染器 | 渲染器 | 与 MekkoRenderer 插件一起使用;将 y 轴显示为从 0 到 1 (0 到 100%)的范围,将 x 轴显示为每个系列的刻度,缩放到所有 y 值的总和。 | | $ jqpsk . MEK korender | 渲染器 | 绘制 Mekko 样式的图表,在二维图形上显示三维数据。 | | $.jqplot.MeterGaugeRenderer | 渲染器 | 绘制仪表图。 | | $ jqlot,移动 | 插件 | jQuery 移动虚拟事件支持。 | | $ jqpplot .奥克雷德尔 | 渲染器 | 绘制开盘-盘高-盘低-收盘图、蜡烛图和盘高-盘低-收盘图。 | | $ jqp lot .复制 | 渲染器 | 画一个饼状图;x 值(如果存在)用作切片标签,y 值给出切片大小。 | | $ . jqplot .点标签 | 插件 | 在数据点放置标签。 | | $ . jqplot . pyramidaxisrenderer | 渲染器 | 与 PyramidRenderer 插件一起使用;在底部显示两个 x 轴,在中心显示 y 轴。 | | $.jqplot.PyramidGridRenderer | 渲染器 | 与 PyramidRenderer 插件一起使用;在`canvas`元素上创建网格。 | | $ . jqlot . pyramids 渲染器 | 渲染器 | 画一个金字塔图。 | | $ jqp lot .趋势线 | 插件 | 自动计算并绘制绘制数据的趋势线。 |