四、JavaScript 的可视化

第 2 章JavaScript 和 HTML5 可视化中,我们研究了使用可伸缩矢量图形构建可视化所提供的优势。 但是,应该清楚的是,通过操纵底层 XML 来构建 svg 是一项令人沮丧且耗时的工作。 尽管有无数的 XML 操作工具,但利用专门为构建 svg 而不是更通用的语言而设计的 API 的强大功能将是很好的。

有许多为操作 svg 而创建的 JavaScript 库。 svg.js(http://www.svgjs.com/)和 Raphaël(http://raphaeljs.com/)都是优秀的绘画工具。 Raphaël 网站上的演示尤其令人印象深刻。 js 提供了专门为可视化设计的功能,我们也将研究它。

【【】Raphael

使用*Raphaël*绘制一个简单的矩形要比在 XML 中构建相同的矩形舒服得多。 该库可以从光盘或从 CloudFlare 等 CDN 中包含,如下代码所示:

**```

我们可以用以下方式画出任何形状:

function drawRectangle() { var paper = Raphael("visualization", 320, 200); paper.rect(50, 20, 50, 150); }

这段代码找到 ID 为`visualization`的元素,并向其添加一个尺寸为 320 x 200 像素的 SVG。 然后在`(50, 20),`处插入一个新的矩形,宽度`50`,高度`150`。

如果我们想用 Raphaël 创建一个简单的列图,这并不困难。 让我们试试吧。 首先我们需要一些数据。 我们现在可以使用 JavaScript 数组,但在现实世界中,这些信息可以以 JSON 或 XML 形式从 web 服务中检索。 在这个例子中,我们将选择几个月和相关的值,如下面的代码所示:

var data = [{month: "May", value: 5}, {month: "June", value: 4}, … {month: "September", value: 8}];

现在,让我们更新上面用来绘制矩形以处理数据值的函数。 首先,我们将更改方法签名,如下面的代码所示:

function drawGraphColumn(paper, item, currentColumn, maximumValue)

此函数接受要绘制的 SVG、当前项和用于计算适当高度的最大值。 在函数体中,我们将从计算杆的高度开始,如下面的代码所示:

var barHeight = (500 * (item.value/maximumValue));

我们已经将最大高度编码为 500px,并且每个栏都是高度的百分比,等于项目的值与最大值的百分比。 我们将使用这个值来绘制条形图,如下面的代码所示:

var rectangle = paper.rect(currentColumn*30, 500 - barHeight, 20, barHeight); rectangle.attr("fill", "rgb("+ (item.value * 40) + " ," + (item.value * 20) + "," + item.value * 20 + ")"); rectangle.attr("stroke-width", 2);

矩形是基于它要避免重叠矩形的列的偏移量。 我们将颜色设置为项目值的函数,以便颜色随高度而变化。

调用该函数的方法是依次将每个数据元素传递给它:

var maximumValue = 0; $.each(data, function(index, item) { if(item.value > maximumValue) maximumValue = item.value; }); $.each(data, function(index, item){ drawGraphColumn(paper, item, index, maximumValue); });

在这里,我们首先从数组中计算最大值。 然后,调用上面为每个元素定义的`drawGraphColumn()`函数。 使用 jQuery 的`each`操作符对数据数组进行循环,该操作符将给定函数应用于数组中的每个元素。 得到的图如下所示:

![Raphaël](graphics/6542OS_04_01.jpg)

Raphaël 是一个通用的 SVG 库。 这意味着,除了适合构建可视化,它还可以用于创建更通用的绘图。 与我们寻找操作 svg 的 API(它比手工操作 XML 更适合)相同,如果有一个用于构建可视化的库就更好了。 js 是一个专门设计用来更容易地使用 SVG 构建可视化的库。

# 【翻译

**d3.js**带来了许多函数和一种编码风格,使创建甚至像上面的一个简单的图表更简单。 让我们用 d3 重新绘制上面的图表,看看它有什么不同。 首先要做的是在页面中引入一个 SVG 元素。 在 Raphaël 中,我们使用构造函数完成了这个操作; 在 d3 中,我们将显式地添加一个 SVG 元素,如下面的代码所示:

var graph = d3.select("#visualization") .append("svg") .attr("width", 500) .attr("height", 500);

马上,您就会看到 JavaScript 的风格与 Raphaël 有很大的不同。 D3 严重依赖于方法链接的使用。 如果你是这个概念的新手,你很快就会学会。 对方法的每次调用都执行一些操作,然后返回一个对象,下一个方法调用对这个对象进行操作。 因此,在本例中,`select`方法返回`div`和`visualization`的`ID`。 在选定的`div`上调用`append`会添加一个 SVG 元素,然后返回它。 最后,`attr`方法在对象内部设置一个属性,然后返回对象。

一开始,方法链接可能看起来很奇怪,但随着我们继续前进,您会发现它是一种非常强大的技术,并且大大简化了代码。 如果没有方法链接,我们最终会得到很多临时变量。

接下来,我们需要找到数据数组中的最大元素。 在前面的例子中,我们使用了 jQuery`each`循环来查找该元素。 D3 内置了数组函数,使其更加清晰,如下面的代码所示:

var maximumValue = d3.max(data, function(item){ return item.value;});

对于求最小值和平均值也有类似的函数。 这些函数都是通过使用 JavaScript 实用程序库(如下划线.js 或 lodash)获得的。 但是,使用内置版本是很方便的。

接下来我们要用到的是 d3 的缩放函数,如下面的代码所示:

var yScale = d3.scale.linear() .domain([maximumValue, 0]) .range([0, 300]);

缩放函数用于从一个数据集映射到另一个数据集。 在我们的例子中,我们将数据数组中的值映射到 SVG 中的坐标。 这里我们使用两种不同的尺度:线性和序数。 线性比例尺用于将连续域映射到连续范围。 映射将线性,所以如果我们的域之间包含值`0`和`10`,和我们的范围值范围之间的`0`和`100`,`6`的值会映射到`60`,`3``30,`等等。 这看起来很琐碎,但是对于更复杂的领域和范围,尺度是非常有用的。 除了线性尺度,还有幂和对数尺度可能更适合你的数据。

在我们的示例数据中,我们的`y`值不是连续的; 它们甚至不是数字。 对于这种情况,我们可以使用序数尺度,如下面的代码所示:

var xScale = d3.scale.ordinal() .domain(data.map(function(item){return item.month;})) .rangeBands([0,500], .1);

顺序比例尺将离散域映射为连续域。 这里,`domain`是月份列表,范围是 SVG 的宽度。 你会注意到我们用`rangeBands`而不是`range`。 范围带将范围分割成块,每个块分配每个范围项目。 如果定义域范围`{May, June}`和`0``100`,可能以后我们会得到一个乐队从`0`到`49,`和【显示】和`100`6 月将`june`。 你还会注意到`rangeBands`有一个附加参数; 在我们的情况下`0.1`。 这是一个填充值,在每个波段之间产生一种无人区。 这是创建条形或柱状图的理想方法,因为我们可能不想让列接触。 填充参数可以接受一个介于`0`和`1`之间的值,作为应该为填充保留多少范围的小数表示。 值为`0.25`将为填充保留 25%的范围。

此外,还有一系列用于提供颜色的内置天平。 为您的可视化选择颜色可能是一个挑战,因为颜色之间的距离必须足够远才能被识别。 如果你像我一样是有色的,那么天平`category10`,`category20`,`category20b`和`category20c`可能适合你。 您可以声明一个颜色刻度,如下代码所示:

var colorScale = d3.scale.category10() .domain(data.map(function(item){return item.month;}));

之前的代码将分配一个不同的颜色,每个月从一组 10 个预先计算的可能的颜色。

最后,我们需要实际绘制我们的图形,如下面的代码所示:

var graphData = graph.selectAll(".bar") .data(data);

我们使用`selectAll`选择图内所有`.bar`元素。 挂在! 图中没有任何元素与`.bar`选择器匹配。 通常,`selectAll`将返回与选择器匹配的元素集合,就像 jQuery 中的`$`函数一样。 在本例中,我们使用`selectAll`作为创建空 d3 集合的简单方法,它有一个`data`方法,可以被链接。

接下来,我们指定一组要与现有元素选择中的数据联合的数据。 D3 在不使用循环技术的情况下操作集合对象。 这允许使用更声明式的编程风格,但可能很难立即掌握。 实际上,我们正在创建两组数据的联合,当前已有数据(使用`selectAll`找到)和新数据(由`data`函数提供)。 这种处理数据的方法可以方便地更新数据元素,以便以后添加或删除更多的元素。

当添加新的数据元素时,您可以使用`enter`()来选择这些元素。 这可以防止对现有数据重复操作。 您不需要重新绘制整个图像,只需要用新数据更新的部分。 同样,如果新数据集中有旧数据集中没有出现的元素,则可以使用`exit()`选择它们。 通常,你只需要删除这些元素,可以通过以下代码完成:

graphData.exit().remove()

当我们使用新生成的数据集创建元素时,数据元素实际上会附加到新创建的 DOM 元素上。 创建元素需要调用`append`,如下代码所示:

graphData.enter() .append("rect") .attr("x", function(item){ return xScale(item.month);}) .attr("y", function(item){ return yScale(item.value);}) .attr("height", function(item){ return 300 - yScale(item.value);}) .attr("width", xScale.rangeBand()) .attr("fill", function(item, index){return colorScale(item.month);});

下图显示了`data()`如何使用新数据集和现有数据集:

![d3.js](graphics/6542OS_04_02.jpg)

您可以在前面的代码中看到方法链接变得多么有用。 与分配一系列临时变量或将结果传递给独立的方法相比,它使代码更短,可读性更强。 在这里,天平也发挥了自己的作用。 x 坐标是通过用序数标度对月份进行缩放得到的。 因为该比例考虑了元素的数量和填充,所以不需要更复杂的内容。

它们的坐标同样可以使用之前定义的`yScale`。 因为 SVG 中的原点在左上角,所以我们必须使用比例的倒数来计算高度。 在这里,除了例子的简洁性,我们一般不会使用常量。 柱的宽度是通过询问`xScale`的宽度来得到的。 最后,我们根据颜色比例设置颜色,如下图所示:

![d3.js](graphics/6542OS_04_03.jpg)

这个 d3 版本的图表是实际上远比 Raphaël 版本更有能力。 我们通过使用天平消除了 Raphaël 中出现的许多神奇数字。

让我们继续增强我们的图,并探索 d3.js 的其他一些特性。

## 自定义颜色比例

我们在上面生成的 d3 图色彩非常丰富,因为我们使用了其中一个内置的颜色尺度。 然而,在大多数情况下,你必须在可视化中保持最少的主题一致性。 您可以通过利用自定义规模实现跨可视化的一致性。

让我们从一个简单的例子开始:交替色阶。 为了制作新的颜色比例,我们需要使用一点 JavaScript 的乐趣来注入一个新的比例函数到 d3 中。 我们首先将函数附加到 d3 的 scale 命名空间,如下面的代码所示:

d3.scale.alternatingColorScale = function() {

JavaScript 允许猴子修补对象,所以这个函数实际上会显示为 d3 的一部分。 我们将通过设置定义域和范围来实现这个函数。 我们定义了`domain`和`range`函数,它们作为域和范围的 getter 和 setter:

var domain, range; scale.domain = function(x){ if(!arguments.length) return domain; domain = x; return scale; } scale.range = function(x){ if(!arguments.length) return range; range = x; return scale; }

最后,我们将建立使用比例尺时调用的映射函数,如下面的代码所示:

function scale(x){ return range[domain.indexOf(x)%range.length]; } return scale; }

以下准则适用此比例尺:

var colorScale = d3.scale.alternatingColor() .domain(data.map(function(item){return item.month;})) .range(["#423A38", "#47B8C8", "#BDB9B1"]);

这就得到了一个更加一致的图,如下图所示:

![Custom color scales](graphics/6542OS_04_04.jpg)

我选择了最简单的条件来定制我们的量表,但也可以使用更复杂和信息更丰富的量表。 使用阈值的量表如下图所示:

![Custom color scales](graphics/6542OS_04_05.jpg)

通过更改`scale`函数并将值而不是键(month)传递给函数,可以很容易地做到这一点。

## 标签和轴

到目前为止,我们已经建立了图表,而没有太多关注我们正在绘制的东西。 如果能在图表上加上一些标签就太好了,这样人们就能轻松解码我们的数据。 幸运的是,d3 提供了一个`axis()`功能,使添加轴成为一个 snap。

我们将从 x 轴开始,如下面的代码所示:

var xAxis = d3.svg.axis().scale(xScale).orient("bottom"); graph.append("g") .attr("transform", "translate(20,300)") .attr("text-anchor", "middle") .call(xAxis);

我们从这里开始使用`axis`功能来创建一个轴。 我们传递我们的`xScale`到给它一个提示,关于刻度上的刻度应该放在哪里。 接下来在图中添加一个`g`元素。 `g`是一个 SVG 元素,它作为一个容器来容纳其他元素。 您可以将任何其他形状放入元素 `g`中,然后对它们进行整体转换。 我们将在接下来的步骤中实现这一点。 默认情况下,坐标轴位于图形的顶部,所以我们将其向下移动并稍微向右移动,以便更好地对齐。 `text-anchor`属性设置文本的 x 坐标应该锚定的位置。 默认情况下,它是左边的,但是由于我们在每个条的中间设置了`text-align:middle`。 最后,我们通过了`xAxis`。

`yAxis`是简单地添加了以下代码:

var yAxis = d3.svg.axis().scale(yScale).orient("left"); graph.append("g") .attr("transform", "translate(20,6)") .call(yAxis); ```

这里的平移有点复杂,我们只是考虑了这样一个事实,我们给了比例向左的方向,这导致它被绘制在最小比例值的左边。 由于我们的最小比例尺值为0,因此我们将其绘制出屏幕,如下图所示:

Labels and axes

我们已经在几行代码中添加了轴和标签。 对于更通用的 SVG 库,这将需要大量的工作。 轴功能也有很多配置选项。 您可以设置刻度的数量、标签和刻度的格式。

小结

虽然我们已经研究了大量 d3,但我们只是触及了 d3.js 的表面功能。 这是一个庞大而强大的图书馆。 关于 d3 的书很多,所以我们不能全讲。 一个伟大的资源 d3 是 Swizec Teller 的书数据可视化与 d3.js,包出版。 当我们在书的其余部分开发一些可视化时,我们将发现大量的附加函数。 我们还将进一步访问本章中给出的函数的一些应用。**