D3-js-快速启动指南-全-

D3.js 快速启动指南(全)

原文:zh.annas-archive.org/md5/f304e06e9445175fb0f626cf86560841

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读 D3.js 快速入门指南。在其中,我们将通过一系列大型构建来介绍 D3 的基础知识。到结束时,你应该对库有足够的了解,可以出去构建自己的交互式数据可视化。

本书面向的对象

本书面向对数据可视化感兴趣的初级到高级前端和全栈 Web 开发者。读者需要对 HTML、CSS、JavaScript、AJAX 以及服务器是什么有基本的了解,才能使用本书中给出的代码和概念。

本书涵盖的内容

第一章,开始使用 D3.js,提供了一个关于 D3 为何如此有趣的概述。我们探讨了 SVG 元素是什么,并设置我们的机器,使其准备好创建 D3 代码。我们还审视了本书的学习方法以及它如何应用于我们将要构建的应用程序。

第二章,使用 SVG 通过代码创建图像,涵盖了 SVG 的基础(基础标签、基本元素、定位和样式)。我们还探讨了贝塞尔曲线以及如何使用它们绘制有机形状。现在我们已经准备好学习如何使用 D3 来修改这些元素。

第三章,构建交互式散点图,解释了静态散点图,并展示了如何构建一个显示其数据的表格。

第四章,制作一个基本的交互式散点图,展示了尽可能多的有用模块,以及基于我们使用它们的经验提供的日常活动示例和个人评论。

第五章,使用数据文件创建柱状图,涵盖了任何系统管理员每天都需要运行的许多有趣用例。我们还可以通过自定义剧本展示许多其他任务。但并非每个脚本都被视为良好的自动化。重要的是,正确的节点能够无错误且在短时间内从状态 A 过渡到状态 B。

第六章,通过动画 SVG 元素创建交互式饼图,展示了当你从饼图中移除部分时,饼图是如何进行动画的。

第七章,使用物理力创建力导向图,展示了如何使用 D3 创建一个可视化数据中各个节点之间关系的图表。这在诸如绘制朋友网络、显示父子公司关系或显示公司员工层级等场景中非常有用。

第八章,映射,讨论了 GeoJSON,它的用途以及为什么它与更通用的 JSON 数据不同。我们还介绍了如何使用 D3 创建投影并将 GeoJSON 数据渲染为地图。

为了充分利用本书

本书假设您对 HTML、CSS、JavaScript、AJAX 以及服务器是什么有基本的了解,以便能够使用本书中给出的代码和概念。

对于本书,您实际上只需要下载并安装以下内容:

  • Chrome,可在www.google.com/chrome/找到:一个网页浏览器,以便我们可以查看我们的可视化。

  • Node:nodejs.org/en/:这允许我们从终端运行 JavaScript。在第四章“制作基本散点图交互”中,我们将使用它来执行 AJAX 调用。

  • 一个代码编辑器。如果您是编程新手,我建议使用 Atom:atom.io/.

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便直接将文件通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 在www.packt.com登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入本书的名称,并遵循屏幕上的说明。

文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/D3.js-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还提供其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789342383_ColorImages.pdf.

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块设置如下:

<circle r=50 cx=50 cy=50 fill=red stroke=blue stroke-width=5></circle>

当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

<head>
       <link rel="stylesheet" href="app.css">
</head>

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“从管理面板中选择系统信息。”

警告或重要注意事项如下所示。

技巧和窍门如下所示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并将邮件发送至 customercare@packtpub.com。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现任何形式的我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或参与一本书籍,请访问 authors.packtpub.com.

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解 Packt 的更多信息,请访问 packtpub.com.

第一章:D3.js 入门

大数据时代已经到来!硬件的进步使得计算机能够以前不可能的方式存储、分析和传输大量信息。数据科学已成为美国最受欢迎的领域之一,公司不断推出新技术来分析客户信息;似乎每天都有新的方法来可视化这些数据。D3 已经成为创建动态、交互式、数据驱动的网络可视化的最受欢迎的库。与之前用于数据可视化的许多技术不同

,D3 利用将 SVG 图像与网络浏览器和 JavaScript 结合起来的力量。在本章中,我们将讨论以下主题:

  • 什么是 SVG?

  • D3 有什么特别之处?

  • 本书的学习方法

什么是 SVG?

在网络上通过交互式图形展示你的数据是最佳方式之一。这种方法的优点是,其交互性允许创作者在一个可视化中包含更多信息,而网络的普遍性则允许任何人立即访问它。PowerPoint 演示文稿或更糟糕的是,将静态图像打印成纸作为讲义的日子已经过去了。有许多方法可以创建基于网络的交互式数据可视化,但没有一种方法比名为 D3.js 的 JavaScript 库更受欢迎。

要理解为什么 D3.js 工作得如此出色,重要的是要了解 SVG 是什么以及它与 D3 的关系。SVG 代表 可缩放矢量图形,它是一种使用数学方向/命令显示形状的方法。传统上,图像的信息存储在网格中,也称为位图。图像的每个方格(称为像素)都有特定的颜色:

图片

但在 SVG 中,存储的是一组简洁的绘图指令。例如,绘制圆的绘图命令如下:

<circle r=50><circle>

这段代码生成的文件大小更小,因为它是一组绘图指令,所以图像可以无损放大。位图图像放大时,会变得模糊和像素化。与矢量图形相比,位图图形的优势在于它们非常适合存储复杂的图像,如照片。对于照片,每个像素可能都有不同的颜色,因此最好使用位图图像。想象一下为照片编写 SVG 绘图命令:你将为每个像素创建一个新的元素,文件大小会变得非常大。

一旦写出了 SVG 绘图命令,程序就需要解释该命令并显示图像。直到最近,只有指定的绘图应用程序,如 Adobe Illustrator,才能查看和操作这些图像。但到了 2011 年,所有主要的现代浏览器都支持 SVG 标签,允许开发者直接在网页上嵌入 SVG。由于 SVG 图像直接嵌入到网页代码中,通常用于操作 HTML 的 JavaScript 可以用来根据用户事件操作图像的形状、大小和颜色。为了使您刚才看到的 SVG 示例中的圆圈增长到原来的两倍大小,JavaScript 只需更改 r 属性:

<circle r=100><circle>

这是一次巨大的突破,它使得复杂的交互式数据可视化能够托管在网络上。

D3 有什么特别之处?

D3.js 在这个时刻出现,因为编写代码以创建复杂的数据驱动文档(D3 得名的由来)——这些文档将 SVG 图像与互联网上可用的海量数据链接起来,是一项艰巨的任务。在奥巴马与罗姆尼总统辩论期间,随着《纽约时报》发布了一系列惊人的可视化作品,D3 开始崭露头角。在此处查看一些示例:

D3 简化了开发者创建基于浏览器的可视化时可能遇到的一些最常见以及一些最复杂的任务。在其核心,D3 可以轻松地将 SVG 图像属性映射到数据值。随着数据值的变化,由于用户交互,图像也会相应变化。

本书的学习方法

D3 是一个庞大的库,包含数百万个选项,但其核心概念很容易学习。您不需要了解库的每个细节就能成为一名功能性的 D3 开发者。相反,这本书试图教授 D3 最基本的部分,以便读者能够快速准备好就业。它通过引导用户通过一系列开发者将被要求制作的常见图表来实现这一点:散点图、条形图、饼图、力导向图和地图。目标是不仅教授基础知识,还要给读者提供一套有趣的构建,这些构建不仅在工作时有用,而且随着他们职业生涯的继续,也有助于他们从中汲取经验。

请注意,这里展示的代码是为了从教育角度易于理解而创建的。它并不是为生产准备的代码。也不使用 ES6 或 ES7 语法。通常,用生产就绪的代码或用 ES6/ES7 编写的代码来展示一个概念可能会阻碍教育体验。假设读者对编程的核心概念足够熟悉,他们可以在熟悉 D3 的基础知识后自行改进代码。

每个构建的预览

每章都专注于特定的构建。每章完成的构建代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide。

使用 SVG 通过代码创建图像

在本章中,我们将学习如何使用 SVG 在浏览器中渲染形状。我们将涵盖如下形状:

  • 圆形:

图片

  • 线条:

图片

  • 矩形:

图片

  • 椭圆:

图片

  • 多边形:

图片

  • 多段线:

图片

  • 立方贝塞尔曲线:

图片

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter02。

构建交互式散点图

在本章中,您将学习如何在图表上绘制点以创建散点图。它看起来可能像这样:

图片

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter03。

使基本的散点图交互式

本章在上一章的基础上进行了扩展,增加了交互功能,允许您执行以下操作:

  • 创建新点:

图片

  • 移除点:

图片

  • 更新点:

  • 缩放和平移:

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter04.

使用数据文件创建条形图

在本章中,我们将学习如何使用 AJAX 在页面加载后进行异步调用,以检索一些 JSON 数据并将其渲染为条形图。它应该看起来如下:

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter05.

使用 SVG 元素动画创建交互式饼图

在本章中,我们将学习如何制作饼图:

然后我们将它转换为饼图:

然后我们将创建功能,使用户能够删除图表的一部分,并且它将以平滑的过渡关闭缺口:

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter06.

使用物理创建力导向图

在本章中,我们将使用力导向图来绘制人与人之间的关系图。它将看起来如下:

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter07.

映射

在第八章,映射中,我们将学习如何使用 GeoJSON 数据创建世界地图。它将看起来如下:

本节完成的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter08.

设置

对于这本书,你实际上只需要下载并安装以下内容:

  • Chrome:www.google.com/chrome/.

    • 一个网络浏览器,以便我们可以查看我们的可视化。
  • 节点:nodejs.org/en/.

    • 这允许我们从终端运行 JavaScript。在第四章,制作基本散点图交互式,我们将使用它来进行 AJAX 调用。
  • 代码编辑器。如果你是编程新手,我建议使用 Atom:atom.io/.

摘要

在本章中,您已经得到了一个关于 D3 为何如此有趣的概述。我们探讨了 SVG 元素是什么,并设置了我们的机器,使其准备好创建 D3 代码。我们还审视了本书的学习方法以及它如何应用于我们将要构建的应用程序。在第二章《使用代码创建 SVG 图像》中,我们将深入探讨创建 SVG 元素。

第二章:使用代码创建图像的 SVG

SVG 元素是在网页内创建图像的一种方式,是 D3 及其工作方式的基础。它们使用代码来创建形状,而不是定义图像的每个像素。本章将介绍如何在网页内创建各种 SVG 元素。其中,我们将涵盖以下主题:

  • 基础标签

  • 基本元素

  • 定位

  • 样式

  • 重要 SVG 元素

本节完整的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter02。

基础标签

在浏览器中查看 SVG 图形时,在 HTML 页面中嵌入一个<svg>标签非常重要。让我们创建一个index.html文件,并将其添加到其中:

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
    </head>
    <body>
        <svg></svg>
    </body>
</html>

现在启动一个网络浏览器并打开该文件(通常,文件 | 打开文件)。对于这本书,建议读者使用 Google Chrome,但在开发和生产中,任何浏览器都可以。如果我们检查 Chrome 的开发者工具(视图 | 开发者 | 开发者工具)中的 HTML,我们会看到以下内容:

图片

基本元素

我们可以通过向<svg>元素添加各种预定义标签作为子元素来绘制我们的元素。这与我们在 HTML 中添加<div>、<a>和<img>标签到<body>标签中的做法一样。有许多标签,如<circle>、<rect>和<line>,我们将在稍后探讨。这里只是一个例子:

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
    </head>
    <body>
        <svg>
            <circle></circle>
        </svg>
    </body>
</html>

注意,我们看不到圆,因为它没有半径,如下面的截图所示:

图片

我们将在稍后讨论这个问题,但,目前,如果我们想看到圆,我们可以添加一个特殊的属性,这是所有<circle>元素都有的:

<circle r=50></circle>

这告诉浏览器给圆一个50像素的半径,如下面的截图所示:

图片

然而,目前我们只能看到圆的右下四分之一。这是因为圆的中心被绘制在<svg>的非常左上角,其余部分被裁剪在<svg>之外。我们可以通过改变圆的位置来改变这一点,我们将在下一部分做。

元素定位

<svg>标签是一个内联元素,例如一个图片(与<div>这样的块元素相对)。<svg>内的元素定位类似于 Photoshop,使用一组遵循(x,y)形式的坐标。一个例子可以是(10,15),这表示x=10和y=15。这与 HTML 不同,在 HTML 中元素是相对于彼此布局的。以下是一些需要注意的重要事项:

  • 点(0,0)是<svg>元素的左上角。

  • 随着y值的增加,点沿着<svg>元素垂直向下移动。

  • 不要与具有 (0,0) 在左下角且点向上移动的典型坐标系混淆,随着 y 值的增加。此图显示了传统坐标系和 SVG 坐标系之间的差异:

我们可以使用负的 x/y 值:

  • -x: 向左移动

  • -y : 向上移动

让我们通过调整 cx 和 cy 值(元素的 x 和 y 中心值)来调整我们之前章节中圆的位置:

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
    </head>
    <body>
        <svg>
            <circle r=50 cx=50 cy=50></circle>
        </svg>
    </body>
</html>

现在我们看到了完整的圆:

元素样式化

任何 <svg> 标签内的标签都可以使用以下属性进行样式化(以下带有示例值的属性):

  • fill=red 或 fill=#ff0000 将改变形状的颜色。

  • stroke=red 或 stroke=#ff0000 将改变线条颜色。线条是围绕每个元素的线条。

  • stroke-width=4 将调整线条的宽度。

  • fill-opacity=0.5 将调整填充颜色的透明度。

  • stroke-opacity=0.5 将调整线条颜色的透明度。

  • transform = "translate(2,3)" 将按给定的 x、y 值平移元素。

  • transform = "scale(2.1)" 将按给定比例(例如,2.1 倍大)缩放元素的大小。

  • transform = "rotate(45)" 将按给定度数旋转元素。

让我们样式化我们之前定位的圆:

<circle r=50 cx=50 cy=50 fill=red stroke=blue stroke-width=5></circle>

现在我们得到这个:

注意,前一个屏幕截图中的线条正在被裁剪。这是因为线条是在元素外部创建的。如果我们想看到完整的线条,我们可以调整圆的大小:

<circle r=45 cx=50 cy=50 fill=red stroke=blue stroke-width=5></circle>

现在我们得到以下输出:

样式也可以使用 CSS 完成。以下步骤将指导你如何使用 CSS 样式化 <svg> 元素:

  1. 在与你的 index.html 文件相同的文件夹中创建一个名为 app.css 的外部文件,并包含以下内容:
      circle {
         fill:red;
         stroke:blue;
         stroke-width:3;
         fill-opacity:0.5;
         stroke-opacity:0.1;
         transform:rotate(45deg) scale(0.4) translate(155px, 
         1px);
         r:50px;
     }
  1. 在 index.html 的 <head> 标签中链接文件:
      <head> <link rel="stylesheet" href="app.css"> </head>
  1. 最后,移除我们之前在 <circle> 标签上设置的行内样式:
      <circle></circle>

现在我们得到这个结果:

注意,我已经在开发者工具中悬停在元素上以显示元素已被旋转 45 度。这就是蓝色框的作用。

重要的 SVG 元素

为了演示每个元素,我们将使用以下代码作为起点,然后在 <svg> 标签内添加每个元素:

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
    </head>
    <body>
        <svg width=800 height=600>
        </svg>
    </body>
</html>

现在我们继续到每个元素。请注意,你可以像我们之前使用 <circle></circle> 一样,以 <element></element> 的形式编写每个标签,或者使用自闭合形式 <element/>,你将在下面看到 <circle/>。

圆形

圆形具有以下属性:

  • r: 半径

  • cx: x 位置

  • cy: y 位置

<circle r="50" cx="200" cy="300"/>

上述代码的输出将如下所示:

线条

线条具有以下属性:

  • x1: 起始 x 位置

  • y1: 起始 y 位置

  • x2: 结束 x 位置

  • y2: 结束 y 位置

这里有两个例子:

<!--
the first element won't be visible because it doesn't have a stroke
the second will be visible because it does have a stroke
-->
<line x1="0" y1="0" x2="100" y2="100"/>
<line x1="0" y1="0" x2="100" y2="100" stroke="purple"/>

以下输出将被显示:

矩形

矩形有以下属性:

  • x: 元素左上角的 x 位置

  • y: 左上角的 y 位置

  • width: 宽度

  • height: 高度

这里有一个例子:

<rect x="50" y="20" width="150" height="150"/>

这段代码会产生以下结果:

椭圆

椭圆有以下属性:

  • cx: x 位置

  • cy: y 位置

  • rx: x 半径

  • ry: y 半径

属性将如下所示:

<ellipse cx="200" cy="80" rx="100" ry="50"/>

输出结果如下所示:

多边形

多边形有以下属性:

  • points,它是一组坐标对

  • 每一对点的形式为 x,y

属性将如下所示:

<polygon points="200,10 250,190 160,210" />

输出结果如下所示:

折线

折线是一系列连接的线。它可以填充,就像多边形一样,但它不会自动重新连接:

<polyline points="20,20 40,25 60,40 80,120 120,140 200,180" stroke="blue" fill="none"/>

输出结果如下所示:

文本

标签的内容是要显示的文本。它有以下属性:

  • x, 元素左上角的 x 位置

  • y, 元素左上角的 y 位置

属性可以使用如下:

<text x="0" y="15">I love SVG!</text>

你可以使用 font-family 和 font-size CSS 样式化这个元素。

组

这个元素没有特殊属性,所以我们将使用转换来定位它。你可以将多个元素放入其中,并且所有定位都将应用于其子元素。这对于将多个元素一起移动非常有用:

<g transform = "translate(20,30) rotate(45) scale(0.5)"></g>

贝塞尔曲线

如果我们想绘制复杂的有机形状呢?为了做到这一点,我们需要使用路径。不过,首先,为了理解路径,你必须了解贝塞尔曲线。

三次贝塞尔曲线

贝塞尔曲线有两种类型:

每条曲线由四个点组成:

  • 起始点

  • 结束点

  • 起始控制点

  • 结束控制点

起始/结束点是曲线开始和结束的位置。控制点定义了曲线的形状。以下图表可以帮助你理解:

当我们操纵控制点时,我们可以看到曲线形状是如何受到影响的:

你甚至可以将多个贝塞尔曲线连接起来,如图所示:

平滑三次贝塞尔曲线

平滑三次贝塞尔曲线只是当它们连接在一起时简化某些三次贝塞尔曲线的一种方式。看看这个图中红色正方形中显示的两个控制点:

正方形的左下角点是第一条曲线的终点控制点。正方形的右上角点是第二条曲线的起点控制点。

注意,这两个点在中心黑色圆点周围是彼此的反射,这个黑色圆点是第一条曲线的终点和第二条曲线的起点。这两个点彼此正好相差 180 度,并且它们与中心点的距离相同。

在这种情况下,一个曲线的起点控制点是前一个曲线的终点控制点的反射,我们可以省略第二个曲线的起点控制点的声明。相反,我们让浏览器根据第一个曲线的终点控制点来计算它:

我们还可以省略起点,因为浏览器知道它将与前一个曲线的终点相同。总之,为了定义第二个曲线,我们只需要两个点:

  • 终点

  • 终点控制点

二次贝塞尔曲线

我们可以简化定义贝塞尔曲线的另一种情况是,当起点控制点和终点控制点相同时:

在这里,我们只需要三个点来定义曲线:

  • 起点

  • 终点

  • 一个既作为起点控制点又作为终点控制点的单个控制点

平滑二次贝塞尔曲线

我们可以简化定义贝塞尔曲线的最终情况是,我们有一个二次贝塞尔曲线(一个单独的控制点),它是前一个曲线的终点控制点的反射:

在这种情况下,浏览器知道曲线的起点(前一个曲线的终点),并且它可以基于前一个曲线的终点控制点计算出所需的单个控制点(因为它是一个二次贝塞尔曲线)。这是一个平滑的二次贝塞尔曲线,您只需要一个点来定义它:

  • 终点

绘制路径

现在我们已经理解了贝塞尔曲线,我们可以在我们的 SVG 中使用 <path> 元素来使用它们。

文档可以在这里找到:developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths.

这些标签接受一个 d 属性,该属性代表一组绘图命令。此属性的值是以下组合中的任何一种:

  • M = moveto: 将绘图点移动到给定的坐标

    • M x y
  • L = lineto: 从 d 命令中的前一个点绘制到给定点的线

    • L x y
  • C = curveto: 从 d 命令中的前一个点绘制曲线到给定控制点的点

    • C x1 y1, x2 y2, x y

    • 第一对是第一个控制点

    • 第二对是第二个控制点

    • 最后一对是曲线的最终终点

  • S = 平滑曲线:

    • S x2 y2, x y

    • 遵循另一个曲线

    • 使用前一个 S 或 C 命令的x2 y2 的反射作为x1 y1

  • Q = 二次贝塞尔曲线:

    • Q x1 y1, x y

    • 使用一个控制点作为起始和结束控制(x1, y1)

  • T = 平滑二次贝塞尔曲线:

    • T x y

    • 遵循另一个曲线

    • 使用前一个二次曲线的控制点的反射作为其控制点

  • Z = 关闭路径:从d命令中的前一个点画一条线到d命令中的第一个点

注意,所有这些命令也可以用小写字母表示。如果使用大写字母,这意味着绝对定位(相对于 SVG 元素的左上角);小写字母表示所有点都相对于d命令中的前一点表示。

让我们用线条来画一个三角形:

<path d="M150 0 L75 200 L225 200 Z" stroke="black" fill="transparent"/>

将显示以下输出:

图片

接下来,我们将绘制一个贝塞尔曲线:

<path d="M0 70 C 0 120, 50 120, 50 70 S 100 20, 100 70" stroke="black" fill="transparent"/>

将显示以下输出:

图片

这是一个二次贝塞尔曲线:

<path d="M0 100 Q 50 50, 100 100 T 200 100 Z" stroke="black" fill="transparent"/>

将显示以下输出:

图片

弧线

弧是一个可以添加到路径中的命令,可以绘制椭圆的一部分。为此,我们开始时只有两个点:

图片

对于任意两个点,只有两个具有相同宽高和旋转的椭圆包含这两个点。在上一个图中,尝试想象在不旋转或缩放椭圆的情况下移动椭圆。一旦这样做,它们就会至少失去与两个给定点中的一个的接触。一个点可能在椭圆上,但另一个则不会。

我们可以使用这些信息来绘制上一个图中显示的四个彩色弧线中的任何一个。

将以下代码部分作为<path>元素上的d属性的值:

A rx ry x-axis-rotation large-arc-flag sweep-flag x y

让我们看看弧线的各种属性:

  • A: 创建一个弧线绘制命令

  • rx: 两个椭圆的x半径(以 px 为单位)

  • ry: 两个椭圆的y半径(以 px 为单位)

  • x-axis-rotation: 旋转两个椭圆一定角度

  • large-arc-flag: 指示是否沿着包含超过 180 度的弧线移动(1 表示移动,0 表示不移动)

  • sweep-flag: 指示是否沿着顺时针方向的弧线移动(1 表示移动,0 表示不移动)

  • x: 目标x值(以 px 为单位)

  • y: 目标y值(以 px 为单位)

large-arc-flag确定是否绘制大于 180 度的弧线。以下是一个没有它的示例(注意,红色显示绘制的弧线,而绿色弧线是使用large-arc-flag和sweep-flag组合可以绘制的其他可能的弧线):

图片

注意,它选择两个较小弧线中的一个。以下是一个将large-arc-flag设置为的示例:

注意,它选择了两个较大弧中的其中一个。

在上一个例子中,对于large-arc-flag被设置或未设置的情况,还有一个其他的弧可以选择。为了确定选择这两个弧中的哪一个,我们使用sweep-flag,它决定了是否从起点顺时针移动到终点。以下是一个large-arc-flag被设置但没有设置sweep-flag的例子:

注意,我们从起点到终点(从左到右)是逆时针移动的。如果我们设置sweep-flag,我们将按顺时针方向移动:

这里是sweep-flag和large-arc-flag的所有可能组合:

下面是一个使用d属性中的弧的path示例代码:

<path d="M10 10 A 50 50 0 0 0 50 10" stroke="black" fill="transparent"/>

看起来是这样的:

在这里尝试不同的弧值:codepen.io/lingtalfi/pen/yaLWJG。

文档

如果需要,您可以在以下位置找到 SVG 元素的完整文档:developer.mozilla.org/en-US/docs/Web/SVG/Element。

摘要

在本章中,我们介绍了 SVG 的基础知识(基础标签、基本元素、定位和样式)。我们还探讨了贝塞尔曲线以及如何使用它们绘制有机形状。现在我们准备好学习如何使用 D3 来修改这些元素。在第三章《构建交互式散点图》中,我们将深入了解D3.js的基础并创建一个交互式散点图。

第三章:构建一个交互式散点图

让我们假设我们已经开始了慢跑,并希望通过散点图来可视化我们作为跑者的进步数据。我们将有一个包含对象的数组,每个对象都有一个日期和距离属性。对于数组中的每个对象,我们将在 SVG 中创建一个圆圈。如果一个对象的distance属性相对较高,其关联的圆圈将在图表上更高。如果一个对象的date属性相对较高(较晚的日期),其关联的圆圈将在图表的右侧更远。

到本节课结束时,你应该能够做到以下几点:

  • 添加指向 D3 库的链接

  • 使用 D3 添加一个<svg>标签并设置其大小

  • 为我们的应用程序创建一些假数据

  • 添加 SVG 圆圈并为其设置样式

  • 创建一个线性刻度

  • 将数据附加到视觉元素上

  • 使用附加到视觉元素的数据来影响其外观

  • 创建一个时间刻度

  • 解析和格式化时间

  • 设置动态域

  • 动态生成 SVG 元素

  • 创建坐标轴

  • 在表格中显示数据

本节完整的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter03。

添加指向 D3 库的链接

我们首先想做的事情是创建一个基本的index.html文件:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
    </body>
</html>

现在在index.html的<body>标签底部添加一个指向 D3 的链接。我们将将其放在底部,以确保脚本在所有其他 HTML 元素加载到浏览器中之后加载:

<body>    
    <script src="img/d3.v5.min.js"></script>
</body>

现在在index.html相同的文件夹中创建app.js。在其中,我们将存储所有的 JS 代码。现在,只需将此代码放入其中以查看它是否工作:

console.log('this works');
console.log(d3);

在index.html的<body>标签底部链接到它。确保它在 D3 脚本标签之后,这样 D3 就会在app.js脚本之前加载:

<body>
    <script src="img/d3.v5.min.js"></script>
    <script src="img/app.js" charset="utf-8"></script>
</body>

就像我们在第二章中做的那样,在 Chrome 中打开index.html,使用 SVG 通过代码创建图像(文件 | 打开文件),并检查你的开发者工具(查看 | 开发者 | 开发者工具)以查看 JavaScript 文件是否正确链接:

图片

使用 D3 添加一个<svg>标签并设置其大小

在index.html中的<indexentry content=" tag:sizing, with D3">顶部,在<body>中,在<indexentry content=" tag:adding">你的脚本标签之前,添加一个<svg>标签:

<body>
    <svg></svg>
    <script src="img/d3.v5.min.js"></script>
    <script src="img/app.js" charset="utf-8"></script>
</body>

如果我们检查开发者工具的元素选项卡,我们会看到svg元素已经被放置。在 Chrome 中,它默认宽度/高度为 300 px/150 px:

图片

在app.js中,删除你之前的console.log语句,并创建变量来保存<svg>标签的宽度和高度:

var WIDTH = 800;
var HEIGHT = 600;

接下来,我们可以使用d3.select()选择一个单一元素,在这种情况下是<svg>元素:

var WIDTH = 800;
var HEIGHT = 600;

d3.select('svg');

d3.select('svg')的返回值是 D3 版本的 svg 元素(如 jQuery),因此我们可以向其添加命令。让我们添加一些样式来调整元素的高度/宽度:

d3.select('svg')
    .style('width', WIDTH)
    .style('height', HEIGHT);

现在,当我们检查开发者工具时,我们会看到<svg>元素已被调整大小:

图片

为我们的应用程序创建一些假数据

在app.js中,让我们创建一个run对象的数组,我故意将日期存储为字符串,这也是很重要的,因为这个数组必须是对象数组,以便与 D3 一起使用)。到目前为止,你的app.js代码应该如下所示:

var WIDTH = 800;
var HEIGHT = 600;

var runs = [
    {
        id: 1,
        date: 'October 1, 2017 at 4:00PM',
        distance: 5.2
    },
    {
        id: 2,
        date: 'October 2, 2017 at 5:00PM',
        distance: 7.0725
    },
    {
        id: 3,
        date: 'October 3, 2017 at 6:00PM',
        distance: 8.7
    }
];

d3.select('svg')
    .style('width', WIDTH)
    .style('height', HEIGHT);

添加 SVG 圆形并为其添加样式

在index.html中,向你的<svg>元素添加三个圆形(每个圆形将代表一次运行):

<svg>
    <circle/>
    <circle/>
    <circle/>
</svg>

在与index.html相同的文件夹中创建app.css,为圆形和我们的svg元素添加一些样式:

circle {
    r:5;
    fill: black;
}
svg {
    border: 1px solid black;
}

在index.html的头部链接到它:

<head>
    <meta charset="utf-8">
    <title></title>
    <link rel="stylesheet" href="app.css">
</head>

现在页面应该如下所示:

图片

创建一个线性scale

我们目前在 SVG 中有三个圆形,在runs数组中有三个对象。D3 做得最好的事情之一是提供将 SVG 元素与数据链接的能力,以便随着数据的变化,SVG 元素也会变化。在本章中,我们将每个圆形链接到runs数组中的一个对象。如果一个对象的distance属性相对较高,其关联的圆形将在图表上更高。如果一个对象的date属性相对较高(较晚的日期),其关联的圆形将更靠右。

首先,让我们根据runs数组中对象的distance属性垂直定位圆形。D3 做的最重要的事情之一是提供将(或map)数据值转换为视觉点以及相反的能力。它是通过scale来实现的。有许多不同类型的scale可以处理许多不同的数据类型,但到目前为止,我们只是使用linear scale,它将数值数据映射到数值视觉点,反之亦然。

在app.js的底部添加以下内容:

var yScale = d3.scaleLinear(); //create the scale 

每次我们创建一个scale时,我们需要告诉它数据中可能存在的最小和最大可能值(这被称为domain)。为了对yScale这样做,在app.js的底部添加以下内容:

yScale.domain([0, 10]); //minimum data value is 0, max is 10

我们还需要告诉scale哪些视觉值对应于数据中的最小/最大值(这被称为range)。为此,在app.js的底部添加以下内容:

//HEIGHT corresponds to min data value
//0 corresponds to max data value
yScale.range([HEIGHT, 0]); 

现在,你的app.js中的最后三行代码应该如下所示:

var yScale = d3.scaleLinear(); //create the scale
yScale.domain([0, 10]); //minimum data value is 0, max is 10
//HEIGHT corresponds to min data value
//0 corresponds to max data value
yScale.range([HEIGHT, 0]);

在前面的代码片段中,范围的第一个(起始)值是HEIGHT(600),第二个(结束)值是 0。数据值的最小值是 0,最大值是 10。通过这样做,我们表示数据点(跑步距离)为 0 应映射到视觉高度值HEIGHT(600):

图片

这是因为运行距离(数据值)越低,我们越希望将视觉点沿Y轴向下移动。记住,Y轴从屏幕顶部的 0 开始,随着我们垂直向下移动而增加值。

我们还表示,距离(运行)为 10 的数据点应该映射到视觉高度为 0:

图片

再次强调,这是因为随着运行距离的增加,我们希望得到一个越来越低的视觉值,这样我们的圆圈就越来越接近屏幕顶部。

如果你需要提醒自己域/范围是什么,你可以通过记录yScale.domain()或yScale.range()来实现。暂时在app.js底部添加以下代码:

//you can get the domain whenever you want like this
console.log(yScale.domain());
//you can get the range whenever you want like this:
console.log(yScale.range());

我们的 Chrome 控制台应该看起来如下所示:

图片

在声明线性比例的范围/域时,我们只需要指定每个的开始/结束值。起始和结束值之间的值将由 D3 计算。例如,要找出与距离值 5 对应的视觉值,使用yScale()。删除之前的两个console.log()语句,并在app.js底部添加以下代码:

console.log(yScale(5)); //get a visual point from a data value

下面是我们的 Chrome 开发者控制台应该看起来是什么样子:

图片

这个日志显示300是有道理的,因为数据值5位于最小数据值0和最大数据值10之间的一半。范围从HEIGHT(600)开始到0结束,所以这两个值之间的一半是 300。

所以,每次你想将数据点转换为视觉点时,就调用yScale()。我们可以反过来,通过调用yScale.invert()将视觉点转换为数据值。要找出与视觉值 450 对应的数据点,删除之前的console.log()语句,并在app.js底部添加以下代码:

//get a data values from a visual point
console.log(yScale.invert(450));

下面是 Chrome 控制台的样子:

图片

这个日志显示2.5是有道理的,因为视觉值 450 是从起始视觉值 600(HEIGHT)到结束视觉值0的 25%。你现在可以删除最后一行console.log()。

将数据附加到视觉元素上

现在让我们将我们的runs数组中的每个 JavaScript 对象连接到 SVG 中的圆圈上。一旦我们这样做,每个圆圈就可以访问其关联的run对象的数据,以确定其位置。在app.js底部添加以下代码:

yScale.range([HEIGHT, 0]);
yScale.domain([0, 10]);
//selectAll is like select,
//but it selects all elements that match the query string
d3.selectAll('circle').data(runs);

如果runs数组中的对象比圆圈多,则额外的对象将被忽略。如果有比对象多的圆圈,那么 JavaScript 对象将按照它们在 DOM 中出现的顺序附加到圆圈上,直到没有更多的对象可以附加。

使用附加到视觉元素上的数据来影响其外观

我们可以通过传递静态值来更改一组 DOM 元素的属性,并且所有选定的元素都将具有该特定值作为属性。暂时将以下内容添加到 app.js 的末尾:

d3.selectAll('circle').attr('cy', 300);

屏幕上应该显示以下内容:

但是现在,由于每个圆圈都附加了我们 runs JavaScript 数据对象之一,我们可以使用该数据设置每个圆圈的属性。我们通过将 .attr() 方法的第二个参数传递回调函数而不是静态值来实现这一点。移除 d3.selectAll('circle').attr('cy', 300); 并将 app.js 的最后一行从 d3.selectAll('circle').data(runs); 调整为以下内容:

d3.selectAll('circle').data(runs)
    .attr('cy', function(datum, index){
        return yScale(datum.distance);
    });

如果我们刷新浏览器,我们应该看到以下内容:

让我们检查我们刚才写了什么。传递给 .attr() 的作为第二个参数的回调函数在选定的每个视觉元素上运行(在这种情况下是每个 circle 元素)。在每次回调执行期间,该回调函数的返回值被分配给当前元素正在设置的任何方面(在这种情况下是 cy 属性)。

回调函数接收两个参数:

  • 当我们调用 .data(runs) 时,附加到特定视觉元素上的 runs 数组中的单个 datum 对象

  • 该 datum 在 runs 数组中的 index

总结来说,这个过程是遍历 SVG 中的每个 circle。对于每个 circle,它查看附加到该 circle 的 run 对象并找到其 distance 属性。然后,它将数据值输入到 yScale() 中,该函数将其转换为相应的视觉点。然后,这个视觉点被分配给该 circle 的 cy 属性。由于每个数据对象都有不同的 distance 值,因此每个 circle 在垂直方向上的位置都不同。

创建时间刻度

让我们根据关联的运行发生的日期水平定位圆圈。首先,创建一个时间刻度。这就像一个线性刻度,但它不是将数值映射到视觉点,而是将日期映射到视觉点。将以下内容添加到 app.js 的底部:

//scaleTime maps date values with numeric visual points
var xScale = d3.scaleTime();
xScale.range([0,WIDTH]); 
xScale.domain([new Date('2017-10-1'), new Date('2017-10-31')]); console.log(xScale(new Date('2017-10-28'))); console.log(xScale.invert(400));

这里是我们控制台应该看起来像什么:

现在,您可以移除两个 console.log() 语句。

解析和格式化时间

注意,我们 runs 数组中对象的 date 属性是字符串,而不是日期对象。这是一个问题,因为 xScale 与所有时间刻度一样,期望其数据值是日期对象。幸运的是,D3 提供了一种简单的方法来将字符串转换为日期,反之亦然。我们将使用一个特殊格式的字符串,根据文档(github.com/d3/d3-time-format#locale_format),告诉 D3 如何将 runs 数组中对象的 date 字符串属性解析为实际的 JavaScript 日期对象。在 app.js 的末尾添加以下内容:

//this format matches our data in the runs array
var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p"); 
console.log(parseTime('October 3, 2017 at 6:00PM'));
var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p");
//this format matches our data in the runs array 
console.log(formatTime(new Date()));

这里是我们的 Chrome 控制台:

当计算圆的 cx 属性时,让我们使用这个方法。删除最后两个 console.log() 语句,并将以下代码添加到 app.js 的底部:

//use parseTime to convert the date string property on the datum object to a Date object.
//xScale then converts this to a visual value
d3.selectAll('circle')
    .attr('cx', function(datum, index){
        return xScale(parseTime(datum.date));
    });

Chrome 应该看起来像这样:

总结来说,这选择了所有的 circle 元素。然后为每个 circle 的 cx 属性设置回调函数的结果。该回调函数为每个 circle 运行,并获取与该 circle 关联的 run 数据对象,并找到其 date 属性(记住它是一个字符串,例如,'October 3, 2017 at 6:00PM')。它将这个字符串值传递给 parseTime(),然后将其转换为实际的 JavaScript 日期对象。然后,将这个日期对象传递给 xScale(),它将日期转换为视觉值。然后,这个视觉值被用于回调函数刚刚运行的 circle 的 cx 属性。由于 runs 数组中对象的每个 date 属性都不同,因此 circles 在水平位置上不同。

设置动态域

目前,我们正在为距离和日期的范围设置任意的最小/最大值。D3 可以找到数据集的最小/最大值,这样我们的图表就可以只显示我们需要的日期范围。我们只需要传递最小/最大方法一个回调函数,该回调函数会在 runs 数组中的每个数据项上被调用。D3 使用回调函数来确定数据对象中比较最小/最大值的属性。

转到以下代码部分:

var yScale = d3.scaleLinear(); //create the scale
yScale.range([HEIGHT, 0]); //set the visual range (for example 600 to 0)
yScale.domain([0, 10]); //set the data domain (for example 0 to 10)

更改为以下代码:

var yScale = d3.scaleLinear(); //create the scale
yScale.range([HEIGHT, 0]); //set the visual range (for example 600 to 0)
var yMin = d3.min(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance; 
})
var yMax = d3.max(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance;
})
//now that we have the min/max of the data set for distance,
//we can use those values for the yScale domain
yScale.domain([yMin, yMax]); 
console.log(yScale.domain());

Chrome 应该看起来如下:

让我们检查我们刚才写的代码。以下代码找到最小距离:

var yMin = d3.min(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance; 
})

D3 遍历 runs 数组(第一个参数)并在数组的每个元素上调用回调函数(第二个参数)。该函数的返回值与其他元素上回调函数的返回值进行比较。最低值被分配给 yMin。对于 d3.max() 也是同样的操作,但用于最高值。

我们可以将最小/最大函数合并为一个 extent 函数,该函数返回一个与 [yMin, yMax] 完全相同的结构的数组。让我们看看我们刚才写的代码:

var yScale = d3.scaleLinear(); //create the scale
yScale.range([HEIGHT, 0]); //set the visual range (for example 600 to 0)
var yMin = d3.min(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance;
})
var yMax = d3.max(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance; 
})
//now that we have the min/max of the data set for distance
//we can use those values for the yScale domain
yScale.domain([yMin, yMax]);
console.log(yScale.domain());

将之前的代码更改为以下代码:

var yScale = d3.scaleLinear(); //create the scale
yScale.range([HEIGHT, 0]); //set the visual range (for example 600 to 0)
var yDomain = d3.extent(runs, function(datum, index){
    //compare distance properties of each item in the data array
    return datum.distance; 
})
yScale.domain(yDomain);

它短多了,对吧?让我们为 xScale 的域做同样的操作。转到以下代码部分:

//scaleTime maps date values with numeric visual points
var xScale = d3.scaleTime(); 
xScale.range([0,WIDTH]);
xScale.domain([new Date('2017-10-1'), new Date('2017-10-31')]);

//this format matches our data in the runs array
var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
//this format matches our data in the runs array 
var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p"); 

更改为以下代码:

var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p");
var xScale = d3.scaleTime();
xScale.range([0,WIDTH]);
var xDomain = d3.extent(runs, function(datum, index){
    return parseTime(datum.date);
});
xScale.domain(xDomain);

注意我们将 parseTime 和 formatTime 上移,以便它们可以在 .extent() 中使用。Chrome 应该看起来如下:

动态生成 SVG 元素

目前,我们只有足够多的 <circle> 元素来适应我们的数据。如果我们不想计算数组中有多少元素呢?D3 可以根据需要创建元素。首先,从 index.html 中删除所有 <circle> 元素。现在,你的 <body> 标签应该如下所示:

<body>
    <svg></svg>
    <script src="img/d3.v5.min.js"></script>
    <script src="img/app.js" charset="utf-8"></script>
</body>

在 app.js 中,找到以下代码部分:

d3.selectAll('circle').data(runs)
    .attr('cy', function(datum, index){
        return yScale(datum.distance);
    });

修改代码以创建圆圈:

//since no circles exist, we need to select('svg')
//so that d3 knows where to append the new circles
d3.select('svg').selectAll('circle')
    .data(runs) //attach the data as before
    //find the data objects that have not yet 
    //been attached to visual elements
    .enter()
    //for each data object that hasn't been attached,
    //append a <circle> to the <svg>
    .append('circle'); 

d3.selectAll('circle')
    .attr('cy', function(datum, index){
        return yScale(datum.distance);
    });

它应该看起来和之前完全一样,但现在为 runs 数组中的每个对象创建了一个圆圈:

这里是对我们刚才所写内容的更深入解释。看看新代码的第一行:

d3.select('svg').selectAll('circle')

这可能看起来是不必要的。为什么不直接使用 d3.selectAll('circle') 呢?嗯,目前还没有 circle 元素。我们将动态地添加 circle 元素,所以 d3.select('svg') 告诉 D3 将它们添加到何处。我们仍然需要 .selectAll('circle'),这样当我们在下一行调用 .data(runs) 时,D3 就知道将 runs 数组中的各种对象绑定到哪些元素上。但是没有 circle 元素可以绑定数据。没关系。.enter() 找到尚未绑定到任何 circle 元素的 run 对象(在这种情况下是所有对象)。然后我们使用 .append('circle') 为 .enter() 找到的每个未绑定的 run 对象添加一个圆圈。

创建坐标轴

D3 可以自动为你生成坐标轴。将以下内容添加到 app.js 的底部:

//pass the appropriate scale in as a parameter
var bottomAxis = d3.axisBottom(xScale);

这创建了一个底部坐标轴生成器,可以用来将坐标轴插入你选择的任何元素中。将以下代码添加到 app.js 的底部,在 SVG 元素内部添加一个 <g> 元素,然后在其中插入一个底部坐标轴:

d3.select('svg')
    .append('g') //put everything inside a group
    .call(bottomAxis); //generate the axis within the group

这就是 Chrome 应该有的样子:

Chrome 显示

我们希望坐标轴位于 SVG 的底部。修改我们刚才写的代码,使其看起来像这样(注意:我们在 .call(bottomAxis) 后面移除了一个; 并添加了 .attr('transform', 'translate(0,'+HEIGHT+')');):

//pass the appropriate scale in as a parameter
var bottomAxis = d3.axisBottom(xScale); 
d3.select('svg')
    .append('g') //put everything inside a group
    .call(bottomAxis) //generate the axis within the group
    //move it to the bottom
    .attr('transform', 'translate(0,'+HEIGHT+')'); 

目前,我们的 SVG 裁剪了坐标轴:

让我们修改 svg 的 CSS,使其不会裁剪超出其边界的任何元素:

svg {
    overflow: visible;    
}

现在看起来不错:

左侧坐标轴非常相似。将以下内容添加到 app.js 的底部:

var leftAxis = d3.axisLeft(yScale);
d3.select('svg')
    .append('g')
    //no need to transform, since it's placed correctly initially
    .call(leftAxis); 

注意:我们不需要设置 transform 属性,因为它最初就在正确的位置:

看起来有点困难,所以让我们在 app.css 的底部添加以下内容:

body {
    margin: 20px 40px;
}

现在我们的坐标轴已经完整:

在表格中显示数据

仅用于调试目的,让我们创建一个表格,将显示所有数据。将 index.html 中的 <body> 标签修改如下:

<body>
    <svg></svg>
    <table>
        <thead>
            <tr>
                <th>id</th>
                <th>date</th>
                <th>distance</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <script src="img/d3.v5.min.js"></script>
    <script src="img/app.js" charset="utf-8"></script>
</body>

D3 也可以用来操作 DOM,就像 jQuery 一样。让我们以这种方式填充 <tbody>。将以下内容添加到 app.js 的底部:

var createTable = function(){
    for (var i = 0; i < runs.length; i++) {
        var row = d3.select('tbody').append('tr');
        row.append('td').html(runs[i].id);
        row.append('td').html(runs[i].date);
        row.append('td').html(runs[i].distance);
    }
}

createTable();

在 app.css 的底部添加一些表格样式:

table, th, td {
   border: 1px solid black;
}
th, td {
    padding:10px;
    text-align: center;
}

调整 svg 的 CSS 以添加底部边距。这将创建图表和表格之间的空间:

svg {
    overflow: visible;
    margin-bottom: 50px;
}

现在浏览器应该看起来是这样的:

摘要

到目前为止,你有一个静态的散点图和一个显示其数据的表格。在第四章,《制作基本散点图交互式》中,我们将学习如何使其交互式。

第四章:制作一个基本的散点图交互

在上一章中,我们创建了一个静态散点图。在本章中,我们将使其交互式,以便我们可以添加、更新和删除运行。你将学习如何执行以下操作:

  • 创建一个点击处理器

  • 删除数据

  • 拖动一个元素

  • 拖动后的数据更新

  • 创建一个缩放行为以缩放元素

  • 缩放/平移时更新坐标轴

  • 更新变换后的点击点

  • 避免在渲染过程中重绘整个屏幕

  • 隐藏轴以外的元素

本节完整的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter04.

创建一个点击处理器

假设我们想让用户点击 <svg> 元素时创建一个新的运行。将以下内容添加到 app.js 的底部:

d3.select('svg').on('click', function(){
    //gets the x position of the mouse relative to the svg element
    var x = d3.event.offsetX;
    //gets the y position of the mouse relative to the svg element
    var y = d3.event.offsetY; 
    //get a date value from the visual point that we clicked on
    var date = xScale.invert(x);
    //get a numeric distance value from
    //the visual point that we clicked on
    var distance = yScale.invert(y); 

    //create a new "run" object
    var newRun = { 
        //generate a new id by adding 1 to the last run's id
        id: runs[runs.length-1].id+1, 
        //format the date object created above to a string
        date: formatTime(date),
        distance: distance //add the distance
    }
    runs.push(newRun); //push the new run onto the runs array
    createTable(); //render the table
});

让我们检查我们刚才写的。注意,d3.select('svg').on('click', function(){ 在 svg 元素上设置了一个点击处理器。传递给 .on() 的第二个参数的匿名函数在用户每次点击 SVG 时都会被调用。一旦进入那个回调函数,我们使用 d3.event.offsetX 获取鼠标在 SVG 内部的 x 位置,并使用 d3.event.offsetY 获取 y 位置。然后我们使用 xScale.invert() 和 yScale.invert() 将 x/y 可视点转换为数据值(日期和距离,分别)。然后我们使用这些数据值创建一个新的运行对象。我们通过获取 runs 数组中最后一个元素的 ID 并加 1 来创建新运行的 ID。最后,我们将新运行推送到 runs 数组并调用 createTable()。

点击 SVG 创建一个新的运行。你可能注意到 createTable() 只是再次添加了所有运行行:

图片

让我们修改 createTable() 函数,使其在运行时清除之前创建的任何行并重新渲染一切。在 app.js 中的 createTable 函数顶部添加 d3.select('tbody').html(''):

var createTable = function(){
    //clear out all rows from the table
    d3.select('tbody').html(''); 
    for (var i = 0; i < runs.length; i++) {
        var row = d3.select('tbody').append('tr');
        row.append('td').html(runs[i].id);
        row.append('td').html(runs[i].date);
        row.append('td').html(runs[i].distance);
    }
}

现在刷新页面并点击 SVG 创建一个新的运行。表格应该看起来像这样:

图片

现在唯一的问题是当你点击 SVG 时没有创建圆圈。为了解决这个问题,让我们将创建 <circle> 元素的代码包裹在一个渲染函数中,并在定义后立即调用 render():

var render = function(){

    var yScale = d3.scaleLinear();
    yScale.range([HEIGHT, 0]);
    yDomain = d3.extent(runs, function(datum, index){
        return datum.distance;
    })
    yScale.domain(yDomain);

    d3.select('svg').selectAll('circle')
        .data(runs)
        .enter()
        .append('circle');

    d3.selectAll('circle')
        .attr('cy', function(datum, index){
            return yScale(datum.distance);
        });

    var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
    var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p");
    var xScale = d3.scaleTime();
    xScale.range([0,WIDTH]);
    xDomain = d3.extent(runs, function(datum, index){
        return parseTime(datum.date);
    });
    xScale.domain(xDomain);
    d3.selectAll('circle')
        .attr('cx', function(datum, index){
            return xScale(parseTime(datum.date));
        });

}
render();

如果你刷新浏览器,你会在控制台看到一个错误。这是因为 bottomAxis 和 leftAxis 使用了现在仅限于 render() 函数内部的 xScale 和 yScale。为了未来的使用,让我们将 xScale 和 yScale 以及创建域/范围的代码从渲染函数中移出:

var parseTime = d3.timeParse("%B%e, %Y at %-I:%M%p");
var formatTime = d3.timeFormat("%B%e, %Y at %-I:%M%p");
var xScale = d3.scaleTime();
xScale.range([0,WIDTH]);
xDomain = d3.extent(runs, function(datum, index){
    return parseTime(datum.date);
});
xScale.domain(xDomain);

var yScale = d3.scaleLinear();
yScale.range([HEIGHT, 0]);
yDomain = d3.extent(runs, function(datum, index){
    return datum.distance;
})
yScale.domain(yDomain);
var render = function(){

    //since no circles exist,
    //we need to select('svg') so that 
    //d3 knows where to append the new circles
    d3.select('svg').selectAll('circle') 
        //attach the data as before
        .data(runs) 
        //find the data objects that have not yet
        //been attached to visual elements
        .enter()
        //for each data object that hasn't been attached, 
        //append a <circle> to the <svg>
        .append('circle');
    d3.selectAll('circle')
        .attr('cy', function(datum, index){
            return yScale(datum.distance);
        });

    d3.selectAll('circle')
        .attr('cx', function(datum, index){
            //use parseTime to convert 
            //the date string property on the datum object 
            //to a Date object, 
            //which xScale then converts to a visual value
            return xScale(parseTime(datum.date)); 
        });

}
render();

现在转到 app.js 的底部并添加一行调用 <svg> 点击处理器的 render():

var newRun = {
    id: runs[runs.length-1].id+1,
    date: formatTime(date),
    distance: distance
}
runs.push(newRun);
createTable();
render(); //add this line

现在当你点击 SVG 时,会出现一个圆圈:

图片

删除数据

让我们在所有 <circle> 元素上设置一个点击处理程序,以便当用户点击 <circle> 时,D3 将从数组中删除该圆圈及其相关数据元素。在上一节中我们编写的 render 函数声明底部添加以下代码。我们这样做是为了确保点击处理程序在圆圈创建后附加:

//put this at the bottom of the render function, 
//so that click handlers are attached when the circle is created
d3.selectAll('circle').on('click', function(datum, index){
    //stop click event from propagating to 
    //the SVG element and creating a run
    d3.event.stopPropagation(); 
    //create a new array that has removed the run 
    //with the correct id. Set it to the runs var
    runs = runs.filter(function(run, index){ 
        return run.id != datum.id;
    });
    render(); //re-render dots
    createTable(); //re-render table
});

让我们检查之前的代码。第一行选择所有 <circle> 元素并为每个元素创建一个点击处理程序。然而,d3.event.stopPropagation(); 阻止点击事件向上冒泡到 SVG。如果我们不添加它,当我们在圆圈上点击时,SVG 上的点击处理程序也会触发。这会在我们尝试删除运行时创建额外的运行。接下来,我们调用以下代码:

runs = runs.filter(function(run, index){
 return run.id != datum.id;
});

这会遍历 runs 数组,并过滤掉任何具有与点击的 <circle> 关联的 datum 的 id 属性匹配的 id 属性的对象。注意,.on('click', function(datum, index){ 中的回调函数接受两个参数:datum,与该 <circle> 关联的运行对象,以及 runs 数组中运行对象的 index。

一旦我们从 runs 数组中过滤出正确的运行对象,我们就调用 render() 和 createdTable() 来重新渲染图表和表格。

但如果我们点击中间的圆圈并检查开发者工具的 元素 选项卡,我们会看到 <circle> 元素没有被删除:

图片

显示 元素的元素选项卡

在之前的屏幕截图中,看起来只有两个圆圈,但实际上中间的那个圆圈的 cx 被设置为 800,cy 被设置为 0。它与相同位置的另一个圆圈重叠。这是因为我们已经从 runs 数组中删除了第二个元素。当我们重新渲染图表时,runs 数组中只有两个对象;在删除中间运行之前,第二个运行对象原本是第三个运行对象。现在它是第二个运行对象,第二个 <circle> 被分配了数据。第三个圆圈仍然保留了旧数据,因此第二个和第三个圆圈具有相同的数据,因此它们被放置在相同的位置。

让我们把圆圈放入 <g> 中,这样当我们删除一个运行时,就可以轻松清除所有圆圈并重新渲染它们。这样,当我们尝试删除它们时,就不会有任何额外的 <circle> 元素散落在周围。这种方法与我们重新渲染表格时所做的类似。调整 index.html 中的 <svg> 元素,使其看起来如下:

<svg>
    <g id="points"></g>
</svg>

现在我们可以每次调用 render() 时清除 <circle> 元素。这有点粗糙,但暂时可以工作。稍后,我们将以更优雅的方式处理。在 render() 函数声明顶部添加 d3.select('#points').html(''); 并将下一行从 d3.select('svg').selectAll('circle') 调整为 d3.select('#points').selectAll('circle'):

//adjust the code at the top of your render function
 //clear out all circles when rendering d3.select('#points').html('');
 //add circles to #points group, not svg d3.select('#points').selectAll('circle') .data(runs) .enter() .append('circle');

现在如果您点击中间的圆圈,元素将从 DOM 中移除:

图片

从 DOM 中移除元素

如果您尝试删除所有圆圈并添加一个新的圆圈,您将得到一个错误:

图片

显示删除所有圆圈并添加一个新圆圈时得到的错误

这是因为我们在 SVG 点击处理程序中创建 newRun 的代码需要一些修改:

var newRun = { //create a new "run" object
    //generate a new id by adding 1 to the last run's id
    id: runs[runs.length-1].id+1, 
    //format the date object created above to a string
    date: formatTime(date), 
    distance: distance //add the distance
}

这是因为当 runs 数组中没有运行元素时,runs[runs.length-1] 尝试访问数组中索引为 -1 的元素。在 <svg> 点击处理程序内部,让我们添加一些代码来处理用户删除所有运行并尝试添加一个新运行的情况:

//inside svg click handler
var newRun = {
    //add this line
    id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1, 
    date: formatTime(date),
    distance: distance
}

如果您删除所有运行并尝试添加一个新的运行,Chrome 现在应该看起来是这样的:

图片

最后,让我们添加一些 CSS,以便我们知道我们在点击圆圈。首先,将 transition: r 0.5s linear, fill 0.5s linear; 添加到您已经为 circle 编写的 CSS 代码中:

circle {
    r: 5;
    fill: black;
    /* add this transition to original code */
    transition: r 0.5s linear, fill 0.5s linear; 
}

然后将此代码添加到 app.css 的底部:

/* add this css for the hover state */
circle:hover {
    r:10;
    fill: blue;
}

当您悬停在圆圈上时,圆圈应该看起来是这样的:

图片

拖动一个元素

我们希望能够通过拖动相关的圆圈来更新一次运行的 数据。为此,我们将使用一个行为,您可以将其视为多个事件处理器的组合。对于拖动行为,有三个回调函数:

  • 当用户开始拖动时

  • 每次用户在释放鼠标按钮之前移动光标时

  • 当用户释放 鼠标 按钮时

每次我们创建一个行为时,都有两个步骤:

  1. 创建行为

  2. 将行为附加到一个或多个元素上

将以下代码放在 render() 函数声明底部:

//put this code at the end of the render function
var drag = function(datum){
    var x = d3.event.x;
    var y = d3.event.y;
    d3.select(this).attr('cx', x);
    d3.select(this).attr('cy', y);
}
var dragBehavior = d3.drag()    
    .on('drag', drag);
d3.selectAll('circle').call(dragBehavior);

现在,您可以拖动这些圆圈,但数据不会更新:

图片

让我们看看这段代码是如何工作的:

var drag = function(datum){
 var x = d3.event.x;
 var y = d3.event.y;
 d3.select(this).attr('cx', x);
 d3.select(this).attr('cy', y);
}

这个 drag 函数将在用户在释放鼠标按钮之前移动光标时作为回调函数使用。它获取鼠标的 x 和 y 坐标,并将被拖动元素(d3.select(this))的 cx 和 cy 值设置为这些坐标。

接下来,我们生成一个拖动行为,在适当的时候调用刚才解释过的 drag 函数:

var dragBehavior = d3.drag()
 .on('drag', drag);

最后,我们将这种行为附加到所有的 <circle> 元素上:

d3.selectAll('circle').call(dragBehavior);

拖动后更新数据

现在我们将添加功能,以便当用户释放鼠标按钮时,与被拖动的圆圈关联的运行对象的数据将得到更新。

首先,让我们创建一个当用户释放鼠标按钮时将被调用的回调函数。在render()函数声明的底部,在var drag = function(datum){之上添加以下代码:

var dragEnd = function(datum){
    var x = d3.event.x;
    var y = d3.event.y;

    var date = xScale.invert(x);
    var distance = yScale.invert(y);

    datum.date = formatTime(date);
    datum.distance = distance;
    createTable();
}

现在将此函数附加到dragBehavior,以便在用户停止拖动圆圈时调用。查看以下代码:

var dragBehavior = d3.drag()
    .on('drag', drag);

更改为以下内容:

var dragBehavior = d3.drag()
    .on('drag', drag)
    .on('end', dragEnd);

现在,一旦你停止拖动圆圈,你应该看到表格中的数据发生变化:

让我们在拖动圆圈的同时改变圆圈的颜色。将以下内容添加到app.css的底部:

circle:active {
    fill: red;
}

当你拖动一个圆圈时,它应该变成红色。

创建一个缩放行为以缩放元素

我们可以创建的另一种行为是缩放/平移能力。一旦这个功能完成,你将能够通过以下操作之一在不同的图形部分进行缩放和平移:

  • 在触摸板上进行两指拖动

  • 滚动鼠标滚轮

  • 在触摸板上进行捏合/展开操作

你还将能够通过在 SVG 元素上点击和拖动来在图表上左右、上下平移。

将以下代码放在app.js的底部:

var zoomCallback = function(){
    d3.select('#points').attr("transform", d3.event.transform);
}

这是当用户尝试缩放或平移时将被调用的回调函数。它所做的只是将缩放或平移操作转换为应用于包含圆圈的<g id="points"></g>元素的transform属性。现在将以下代码添加到app.js的底部以创建zoom行为并将其附加到svg元素:

var zoom = d3.zoom()
    .on('zoom', zoomCallback);
d3.select('svg').call(zoom);

现在,如果我们缩放,图表应该看起来像这样:

在缩放和平移时更新坐标轴

现在我们缩放时,点移动到内部/外部。当我们平移时,它们垂直/水平移动。不幸的是,坐标轴没有相应更新。让我们首先为包含它们的<g>元素添加 ID。找到以下代码:

var bottomAxis = d3.axisBottom(xScale);
d3.select('svg')
    .append('g')
    .call(bottomAxis)
    .attr('transform', 'translate(0,'+HEIGHT+')');
var leftAxis = d3.axisLeft(yScale);
d3.select('svg')
    .append('g')
    .call(leftAxis);

在第一个.append('g')之后添加.attr('id', 'x-axis'),在第二个.append('g')之后添加.attr('id', 'y-axis'):

d3.select('svg')
    .append('g')
    .attr('id', 'x-axis') //add an id
    .call(bottomAxis)
    .attr('transform', 'translate(0,'+HEIGHT+')');
var leftAxis = d3.axisLeft(yScale);
d3.select('svg')
    .append('g')
    .attr('id', 'y-axis') //add an id
    .call(leftAxis);

现在让我们使用这些 ID 在缩放时调整坐标轴。找到以下代码:

var zoomCallback = function(){
    d3.select('#points').attr("transform", d3.event.transform);
}

将以下内容添加到函数声明的末尾:

d3.select('#x-axis')
    .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale)));
d3.select('#y-axis')
    .call(leftAxis.scale(d3.event.transform.rescaleY(yScale)));

现在zoomCallback应该如下所示:

var zoomCallback = function(){
    d3.select('#points').attr("transform", d3.event.transform);
    d3.select('#x-axis')
      .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale)));
    d3.select('#y-axis')
      .call(leftAxis.scale(d3.event.transform.rescaleY(yScale)));
}

关于前面的代码,有两点需要注意:

  • bottomAxis.scale()告诉坐标轴重新绘制自己。

  • d3.event.transform.rescaleX(xScale)返回一个值,指示底部坐标轴应该如何缩放。

现在当你缩放时,坐标轴应该重新绘制:

变换后更新点击点

尝试缩放和平移,然后点击 SVG 创建一个新的运行。你会注意到它位置不正确。这是因为 SVG 点击处理程序不知道发生了缩放或平移。目前,无论你缩放或平移多少,点击处理程序仍然将其转换为好像你从未缩放或平移过。

当我们缩放时,我们需要将变换信息保存到变量中,以便我们稍后可以使用它来确定如何正确创建圆圈和运行。找到 zoomCallback 声明,并在它之前添加 var lastTransform = null。然后,在函数声明开始处添加 lastTransform = d3.event.transform;。它应该看起来如下:

var lastTransform = null; //add this
var zoomCallback = function(){
    lastTransform = d3.event.transform; //add this
    d3.select('#points').attr("transform", d3.event.transform);
    d3.select('#x-axis')
      .call(bottomAxis.scale(d3.event.transform.rescaleX(xScale)));
    d3.select('#y-axis')
      .call(leftAxis.scale(d3.event.transform.rescaleY(yScale)));
}

现在每当用户缩放或平移时,用于缩小或移动 SVG 和坐标轴的变换数据将保存在 lastTransform 变量中。在点击 SVG 时使用该变量。

在 SVG 点击处理器的开头找到这两行:

var x = d3.event.offsetX;
var y = d3.event.offsetY;

将它们更改为以下内容:

var x = lastTransform.invertX(d3.event.offsetX);
var y = lastTransform.invertY(d3.event.offsetY);

你的点击处理器现在应该看起来像这样:

d3.select('svg').on('click', function(){
    var x = lastTransform.invertX(d3.event.offsetX); //adjust this
    var y = lastTransform.invertY(d3.event.offsetY); //adjust this

    var date = xScale.invert(x)
    var distance = yScale.invert(y);

    var newRun = {
        id: ( runs.length > 0 ) ? runs[runs.length-1].id+1 : 1,
        date: formatTime(date),
        distance: distance
    }
    runs.push(newRun);
    createTable();
    render();
});

但现在在缩放被破坏之前点击,因为 lastTransform 将为空:

图片

找到我们刚刚为 SVG 点击处理器编写的代码:

var x = lastTransform.invertX(d3.event.offsetX);
var y = lastTransform.invertY(d3.event.offsetY);

调整它,使其看起来如下:

var x = d3.event.offsetX;
var y = d3.event.offsetY;

if(lastTransform !== null){
    x = lastTransform.invertX(d3.event.offsetX);
    y = lastTransform.invertY(d3.event.offsetY);
}

初始时,x 和 y 分别被设置为 d3.event.offsetX 和 d3.event.offsetY。如果发生缩放或平移,lastTransform 将不会为空,因此我们将使用变换后的值覆盖 x 和 y。

初始时添加一个新的运行:

图片

现在向右平移并添加一个新的点:

图片

避免在渲染过程中重新绘制整个屏幕

目前,每次我们调用 render() 时,我们都会从 <svg> 中擦除所有 <circle> 元素。这是低效的。我们只需移除我们不需要的元素

在 render() 函数的顶部,将 d3.select('#points').selectAll('circle').data(runs) 赋值给一个变量,这样我们就可以稍后使用它。这有助于保留在下一节中 DOM 元素如何分配给数据元素。在 render() 函数声明顶部找到它:

d3.select('#points').html('');
d3.select('#points').selectAll('circle')
 .data(runs)
 .enter()
 .append('circle');

更改为以下内容:

d3.select('#points').html('');
var circles = d3.select('#points')
 .selectAll('circle')
 .data(runs);
circles.enter().append('circle');

接下来,删除 d3.select('#points').html(''); 这一行。我们将使用 .exit() 来找到尚未与数据匹配的圆圈选择,然后我们将使用 .remove() 来移除这些圆圈。在我们刚刚写的最后一行之后添加以下内容 (circles.enter().append('circle');):

circles.exit().remove();

重新加载页面,点击中心(第二个)圆圈。你会注意到圆圈似乎消失了,右上角的圆圈短暂地获得悬停状态然后又缩小。这并不是真正发生的事情。

如果我们点击中间的圆圈(第二个),它将删除 runs 数组中的第二个运行对象,第三个运行对象将向下移动以替换它。现在我们只有一个包含两个运行对象的数组:第一个和原本是第三个(但现在变成了第二个)。当再次调用 render() 时,原本的中间(第二个)圆圈被分配给原本是 runs 数组中的第三个运行对象(但现在变成了第二个)。这个“运行”对象原本被分配给右上角的第三个圆圈。但现在,由于只有两个运行对象,当我们调用 circles.exit().remove(); 时,第三个(右上角)圆圈将被删除。第二个圆圈的数据已经改变,它跳到右上角以匹配该数据。它原本有一个悬停状态,但突然它从光标下移开,因此它缩小回正常大小并变成黑色。

为了避免这些影响,我们需要确保在调用 render() 时,每个圆圈都保持与它最初分配到的数据一致。为此,我们可以告诉 D3 通过 ID 而不是索引将 <circles> 映射到数据项。在 render() 函数的顶部,找到以下代码:

var circles = d3.select('#points')
 .selectAll('circle')
 .data(runs);

改为如下所示:

var circles = d3.select('#points')
 .selectAll('circle')
 .data(runs, function(datum){
 return datum.id
});

这告诉 D3 在确定将数据对象分配给哪个 <circle> 元素时使用每个运行对象的 id 属性。它基本上是将运行对象的 id 属性最初分配给 <circle> 元素。这样,当第二个运行对象被删除时,circles.exit().remove(); 将找到具有相应 ID(中间的圆圈)的圆圈并将其删除。

现在点击中间的圆圈应该可以正常工作。

隐藏超出轴的元素

如果你进行大量的平移或缩放,你会注意到圆圈在轴的范围之外可见:

要隐藏超出轴的元素,我们只需在 index.html 中的当前 <svg> 元素周围添加一个带有 id="container" 的外部 SVG:

<svg id="container">
    <svg>
        <g id="points"></g>
    </svg>
</svg>

现在将所有 d3.select('svg') 代码替换为 d3.select('#container')。你可以进行查找和替换。应该有五个实例需要更改:

d3.select('#container')
    .style('width', WIDTH)
    .style('height', HEIGHT);

//
// lots of code omitted here, including render() declaration...
//

var bottomAxis = d3.axisBottom(xScale);
d3.select('#container')
    .append('g')
    .attr('id', 'x-axis')
    .call(bottomAxis)
    .attr('transform', 'translate(0,'+HEIGHT+')');

var leftAxis = d3.axisLeft(yScale);
d3.select('#container')
    .append('g')
    .attr('id', 'y-axis')
    .call(leftAxis);

//
// code for create table omitted here...
//

d3.select('#container').on('click', function(){
    //
    // click handler functionality omitted
    //
});

//
// zoomCallback code omitted here
//

var zoom = d3.zoom()
    .on('zoom', zoomCallback);
d3.select('#container').call(zoom); 

最后,调整 CSS 以将 svg { 替换为 #container {:

#container {
    overflow: visible;
    margin-bottom: 50px;
}

现在,一旦圆圈移动到内 <svg> 元素的范围之外,它们应该被隐藏:

摘要

在本章中,我们学习了 D3 的基础知识,并创建了一个完全交互式的散点图。在下一章中,我们将学习如何使用 AJAX 发出异步请求以填充条形图。

第五章:使用数据文件创建条形图

AJAX 代表 Asynchronous JavaScript And XML。基本上,我们可以使用 JavaScript 在页面加载后加载数据。这是一种根据用户交互生成图表的绝佳方式。在本章中,我们将使用 AJAX 来构建条形图。到本章结束时,你应该能够做到以下事情:

  • 使用 AJAX 对外部数据文件进行异步调用

  • 创建条形图

本节完整的代码可以在以下位置找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter05。

设置我们的应用程序

让我们在 index.html 中创建我们的标准设置:

<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <link rel="stylesheet" href="app.css">
    </head>
    <body>
        <svg></svg>
        <script src="img/d3.v5.min.js"></script>
        <script src="img/app.js" charset="utf-8"></script>
    </body>
</html>

现在将以下代码添加到 app.js 中:

var WIDTH = 800;
var HEIGHT = 600;

d3.select('svg')
    .style('width', WIDTH)
    .style('height', HEIGHT);

现在将以下代码添加到 app.css 中:

svg {
    border:1px solid black;
}

这应该是我们应有的结果:

图片

创建一个外部文件来保存我们的数据

让我们创建一个 data.json 文件,它将保存有关工作帖子需要某些技能频率的虚假数据。该文件的内容应该是这样的:

[
  {
    "name": "HTML",
    "count": 21
  },
  {
    "name": "CSS",
    "count": 17
  },
  {
    "name": "Responsive Web Design",
    "count": 17
  },
  {
    "name": "JavaScript",
    "count": 17
  },
  {
    "name": "Git",
    "count": 16
  },
  {
    "name": "Angular.js",
    "count": 9
  },
  {
    "name": "Node.js",
    "count": 9
  },
  {
    "name": "PostgreSQL",
    "count": 8
  },
  {
    "name": "Agile Project Management",
    "count": 8
  },
  {
    "name": "MongoDB",
    "count": 7
  },
  {
    "name": "Trello",
    "count": 7
  },
  {
    "name": "Testing / TDD",
    "count": 7
  },
  {
    "name": "jQuery",
    "count": 7
  },
  {
    "name": "User Testing",
    "count": 6
  },
  {
    "name": "MySQL",
    "count": 6
  },
  {
    "name": "PHP",
    "count": 6
  },
  {
    "name": "React.js",
    "count": 6
  },
  {
    "name": "AJAX",
    "count": 6
  },
  {
    "name": "Express.js",
    "count": 5
  },
  {
    "name": "Heroku",
    "count": 5
  },
  {
    "name": "Wireframing",
    "count": 5
  },
  {
    "name": "Sass/SCSS",
    "count": 5
  },
  {
    "name": "Mobile Web",
    "count": 4
  },
  {
    "name": "Rails",
    "count": 4
  },
  {
    "name": "WordPress",
    "count": 4
  },
  {
    "name": "Drupal",
    "count": 3
  },
  {
    "name": "Ruby",
    "count": 3
  },
  {
    "name": "Ember.js",
    "count": 3
  },
  {
    "name": "Python",
    "count": 3
  },
  {
    "name": "Amazon EC2",
    "count": 2
  },
  {
    "name": "Computer Science degree",
    "count": 1
  },
  {
    "name": "Backbone.js",
    "count": 1
  },
  {
    "name": "Less",
    "count": 1
  },
  {
    "name": "Prototyping",
    "count": 1
  },
  {
    "name": "Redis",
    "count": 1
  }
]

发起 AJAX 请求

现在我们将使用 JavaScript 来请求一些数据。

编写基本代码

D3 有许多不同的方法用于向不同数据类型的文件发起 AJAX 请求:

d3.json('path').then(function(data){
    //do something with the json data here
});
d3.csv('path').then(function(data){
    //do something with the csv data here
});
d3.tsv('path').then(function(data){
    //do something with the tsv data here
});
d3.xml('path').then(function(data){
    //do something with the xml data here
});
d3.html('path').then(function(data){
    //do something with the html data here
});
d3.text('path').then(function(data){
    //do something with the text data here
});

由于我们的数据是 JSON 格式,我们将使用第一种调用方式。将以下内容添加到 app.js 的末尾:

d3.json('data.json').then(function(data){ console.log(data); });

处理文件访问

如果你直接在 Chrome 中打开了 index.html 文件,而不是通过 web 服务器提供服务,你会注意到我们遇到了一个错误。检查你的开发者控制台:

图片

这里的问题是,网络浏览器不应该向你的电脑上的文件发起 AJAX 请求。如果它们可以这样做,这将是一个巨大的安全漏洞,因为任何网站都可以访问你的电脑上的文件。让我们创建一个基本的文件服务器。为此,你需要安装 Node.js (nodejs.org/en/)。一旦完成,打开你的电脑的终端:

  • 对于 Mac:按住 command + Space,然后输入 terminal 并按 Enter。

  • 对于 Windows:点击开始,输入 cmd 并按 Enter。

接下来,在你的终端中输入以下内容:

npm install -g http-server

如果你收到错误消息,尝试这样做:

sudo npm install -g http-server

这安装了一个使用 Node.js 构建的 http-server。要运行它,使用终端导航到保存代码的目录(在终端中输入 cd 以更改文件夹)并运行以下命令:

http-server .

你应该看到类似以下的内容:

图片

现在请在浏览器中访问 http://localhost:8080/。你应该现在能看到你的 AJAX 调用正在成功执行(如果你遇到问题,按住 shift 键并点击刷新按钮,强制浏览器重新加载可能已被缓存的全部文件):

图片

使用 AJAX 数据创建 SVG 元素

现在我们 AJAX 调用成功,让我们开始构建我们的应用。从现在开始,我们将使用基本的 JavaScript 和 D3。请注意,本节课剩余部分我们将要在 AJAX 请求的成功回调中编写代码。在生产环境中,我们可能希望将此代码移至其他位置,但为了学习,这样做更简单。让我们为条形图创建一些矩形。现在app.js(AJAX 请求的回调)的底部应该如下所示:

d3.json('data.json').then(function(data){
    d3.select('svg').selectAll('rect')
        .data(data)
        .enter()
        .append('rect');
});

我们开发工具中的元素标签页应该看起来像这样:

调整条形的高度和宽度

让我们创建一个比例尺,将data中每个元素的count属性映射到相应条形的视觉高度。我们将使用线性比例尺。记住将图表的HEIGHT映射到非常低的数据点,并将图表的顶部(范围中的0)映射到非常高的数据值。将此代码添加到 AJAX 回调的底部:

var yScale = d3.scaleLinear();
yScale.range([HEIGHT, 0]);
var yMin = d3.min(data, function(datum, index){
    return datum.count;
})
var yMax = d3.max(data, function(datum, index){
    return datum.count;
})
yScale.domain([yMin, yMax]);

我们可以使用d3.extent,但稍后我们需要单独的最小值。在上一段代码之后,让 D3 使用yScale调整矩形的高度。记住,y轴是反转的。低数据值产生高范围值。尽管范围很高,条形本身应该很小。我们需要仅对高度重新翻转值,以便低数据值产生小的条形,高数据值产生大的条形。为此,让我们从图表的HEIGHT中减去范围点。这样,如果yScale(datum.count)产生,比如说,500,条形的高度将是 100。我们可以在调整条形位置时正常使用yScale(datum.count)。将以下内容添加到 AJAX 回调的底部:

d3.selectAll('rect')
    .attr('height', function(datum, index){
        return HEIGHT-yScale(datum.count);
    });

现在我们的矩形有了高度,但没有宽度:

在app.css的底部,让我们给所有条形设置相同的宽度:

rect {
    width: 15px;
}

现在我们应该在 Chrome 中看到以下内容:

调整条形的水平和垂直位置

我们的所有条形目前都相互重叠。让我们通过将x的位置映射到数据数组的索引来使它们分散。在 AJAX 回调的底部添加以下内容:

var xScale = d3.scaleLinear(); xScale.range([0, WIDTH]); xScale.domain([0, data.length]); d3.selectAll('rect') .attr('x', function(datum, index){ return xScale(index); });

这将数组中的索引映射到水平范围点。Chrome 应该如下所示:

现在让我们将条形移动,使它们从底部生长,而不是从顶部悬挂。将以下内容添加到 AJAX 回调的末尾:

d3.selectAll('rect')
    .attr('y', function(datum, index){
        return yScale(datum.count);
    });

使用我们的yScale函数,高数据值产生低范围值,这不会使大条形下降很多。低数据点产生高范围值,这会使小条形下降很多。

我们最后几条形没有任何高度,因为我们已经将数据的最小计数属性映射到yScale中的视觉范围值 0。让我们调整此代码的最后一行:

var yScale = d3.scaleLinear();
yScale.range([HEIGHT, 0]);
var yMin = d3.min(data, function(datum, index){
    return datum.count;
})
var yMax = d3.max(data, function(datum, index){
    return datum.count;
})
yScale.domain([yMin, yMax]);

我们将将其更改为以下代码:

var yScale = d3.scaleLinear();
yScale.range([HEIGHT, 0]);
var yMin = d3.min(data, function(datum, index){
    return datum.count;
})
var yMax = d3.max(data, function(datum, index){
    return datum.count;
})
yScale.domain([yMin-1, yMax]); //adjust this line

现在的域最小值比我们数据集中实际存在的值少一个。具有原始最小值的域被视为高于图形最小值预期的值。我们得到以下结果:

使条形宽度动态化

目前,我们的条形宽度是固定的。无论我们有多少个元素,它们的宽度都是 15 px。如果我们有更多的数据元素,条形可能会重叠。让我们改变这一点。由于每个rect的宽度都相同,无论数据如何,我们只需将width分配一个计算值即可。将以下代码添加到 AJAX 回调的末尾:

d3.selectAll('rect')
    .attr('width', WIDTH/data.length);

现在让我们调整我们的rect CSS,使我们的条形更明显:

rect {
    /*  remove the width rule that was here */
    stroke:white;
    stroke-width:1px;
}

输出将如下所示:

根据数据更改条形颜色

目前,条形是黑色的。线性比例尺将在颜色之间进行插值,就像常规数字一样。将以下代码添加到 AJAX 回调的末尾:

var yDomain = d3.extent(data, function(datum, index){
    return datum.count;
})
var colorScale = d3.scaleLinear();
colorScale.domain(yDomain)
colorScale.range(['#00cc00', 'blue'])
d3.selectAll('rect')
    .attr('fill', function(datum, index){
        return colorScale(datum.count)
    })

注意我们使用d3.extent来计算域,以便使用数据集的真实最小值来映射#00cc00:

添加轴

左轴与第四章中所示相同,制作基本散点图交互式。将以下代码添加到 AJAX 回调的底部:

var leftAxis = d3.axisLeft(yScale);
d3.select('svg')
    .append('g').attr('id', 'left-axis')
    .call(leftAxis);

要创建底部轴,我们需要能够将字符串映射到域上的点。我们将为此使用一个带状比例尺,它只是将范围分割成相等的部分,并将其映射到一个离散值数组(例如,不能插值的值)。将以下代码添加到 AJAX 回调的底部:

var skillScale = d3.scaleBand();
var skillDomain = data.map(function(skill){
    return skill.name
});
skillScale.range([0, WIDTH]);
skillScale.domain(skillDomain);

注意我们使用了data.map()。这是常规 JavaScript,它只是遍历一个数组并根据给定的函数修改每个元素。然后它返回结果数组,同时保持原始数组不变。在上一个例子中,skillDomain将是一个包含每个数据元素各种名称属性的数组。

一旦我们有了每个技能的数组,我们就使用它作为域,并将每个技能映射到范围内的一个点。记住,范围中的点是通过根据域中元素的数量将整个范围平均分割来创建的。

现在我们有一个将每个技能文本映射到x范围内一个点的比例尺,我们可以像以前一样创建底部轴。将以下代码添加到 AJAX 回调的底部:

var bottomAxis = d3.axisBottom(skillScale);
d3.select('svg')
    .append('g').attr('id', 'bottom-axis')
    .call(bottomAxis)
    .attr('transform', 'translate(0,'+HEIGHT+')');

我们仍然需要停止<svg>元素裁剪轴。更改app.css中svg的 CSS:

svg {
    overflow: visible;
}

以下为结果:

底部轴文本非常混乱。让我们在app.css的底部添加一些 CSS 来修复这个问题:

#bottom-axis text {
    transform:rotate(45deg);
}

输出将如下所示:

它是旋转的,但它围绕元素的中心旋转。让我们在刚才写的代码中添加一行,使其围绕文本的起始位置旋转:

#bottom-axis text {
    transform:rotate(45deg);
    text-anchor: start; /* add this line */
}

输出将如下所示:

让我们将图表移动到右边,这样我们就可以看到左侧轴的值。调整我们的 svg CSS 代码,使其看起来如下所示:

svg {
    overflow: visible;
    margin-left: 20px; /* add this line */
}

摘要

在本章中,我们学习了如何使用 AJAX 来发送异步请求,以便填充条形图。在第六章,“通过动画 SVG 元素创建交互式饼图”,我们将创建一个当你从其中移除部分时动画显示的饼图。

第六章:通过动画 SVG 元素创建交互式饼图

在本章中,我们将使用动画使我们的图表移动。这可以使你的可视化看起来更加精致和专业。

在本节中,我们将涵盖以下主题:

  • 创建序数尺度

  • 创建颜色尺度

  • 为每个饼图部分添加路径

  • 生成创建弧线的函数

  • 格式化用于弧线的数据

  • 调整饼图的位置

  • 制作一个环形图

  • 移除饼图的部分

本节完整的代码可以在github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter06找到。

设置应用程序

和往常一样,我们需要一个index.html文件来存放我们的 SVG 代码。让我们创建这个文件,并将以下代码添加到其中:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="img/d3.v5.min.js"></script>
    </head>
    <body>
        <svg>
            <g></g>
        </svg>
        <script src="img/app.js" charset="utf-8"></script>
    </body>
</html>

创建数据/配置变量

在<body>标签的底部,我们引用了一个app.js文件。让我们创建这个文件,并将以下代码添加到其中:

var WIDTH = 360;
var HEIGHT = 360;
var radius = Math.min(WIDTH, HEIGHT) / 2;

var dataset = [
    { label: 'Bob', count: 10 },
    { label: 'Sally', count: 20 },
    { label: 'Matt', count: 30 },
    { label: 'Jane', count: 40 }
];
console.log(dataset);

为了确保它正在正常工作并且正确链接,我们在底部添加了console.log(dataset)。让我们在 Chrome 中打开index.html并查看开发者控制台,以确保一切按预期连接:

图片

一旦我们确定它正在正常工作,我们可以移除console.log(dataset);,如下所示:

var WIDTH = 360;
var HEIGHT = 360;
var radius = Math.min(WIDTH, HEIGHT) / 2;

var dataset = [
    { label: 'Bob', count: 10 },
    { label: 'Sally', count: 20 },
    { label: 'Matt', count: 30 },
    { label: 'Jane', count: 40 }
];

创建序数尺度

一个序数尺度将一个离散值映射到另一个值。离散值是那些不能被分割的东西。之前,我们使用过可以分割和插值的数值,比如数字。插值只是意味着对于任意两个数字,我们可以在它们之间找到其他数字。例如,给定 10 和 5,我们可以在它们之间找到值(6,8.2,7,9.9 等等)。现在,我们想要映射那些不能插值的值——我们数据集中的标签属性(Bob,Sally,Matt 和 Jane)。Bob 和 Sally 之间有什么值?Bob 和 Matt 之间呢?没有。这些只是字符串,不是可以分割和插值的数值。

我们想要做的是将这些离散值映射到其他值。以下是一个使用序数尺度如何做到这一点的示例。将以下内容添加到app.js的底部:

var mapper = d3.scaleOrdinal();
mapper.range([45, 63, 400]); //list each value for ordinal scales, not just min/max
mapper.domain(['Bob', 'Sally', 'Zagthor']); //list each value for ordinal scales, not just min/max

console.log(mapper('Bob'));
console.log(mapper('Sally'));
console.log(mapper('Zagthor'));

之前的代码应该产生以下结果:

图片

注意,当你使用序数尺度时,你需要列出域和范围的所有值。即使其中一个集合是数值的(在前一个例子中,范围),你仍然必须列出每个值。如果我们只列出了范围的 min/max,省略了 63,D3 将不知道将 Sally 映射到哪个值。毕竟,Sally 与 Bob 有多接近,作为一个值?Sally 与 Zagth 有多接近,作为一个值?没有办法计算这个距离,因为它们都是文本字符串,不是数字。

令人惊讶的一点是,你无法反转序数刻度。移除之前的三个 console.log() 语句,并暂时将以下内容添加到 app.js 的底部:

console.log(mapper.invert(45));

以下内容将被显示:

图片

D3 只能单向运行:从领域到范围。你现在可以移除 console.log() 语句。

创建将标签映射到颜色的颜色刻度

现在,我们想要将数据集的标签属性映射到颜色,而不是像上一节中那样映射随机数。我们可以制定自己的颜色方案,或者从 D3 的颜色集中选择一个,请参阅 github.com/d3/d3-scale-chromatic#categorical。

如果我们愿意,我们可以看到这些颜色方案只是数组。暂时,将以下内容添加到 app.js 的底部:

console.log(d3.schemeCategory10)

以下内容将被显示:

图片

因此,我们可以在设置范围时使用颜色方案。用以下内容替换之前的 console.log() 语句:

var colorScale = d3.scaleOrdinal();
colorScale.range(d3.schemeCategory10);

我们可以通过使用 JavaScript 的原生 map 函数为该领域生成一个标签数组。将以下内容添加到 app.js 的底部:

colorScale.domain(dataset.map(function(element){
    return element.label;
}));

以下是我们到目前为止的代码:

var WIDTH = 360;
var HEIGHT = 360;
var radius = Math.min(WIDTH, HEIGHT) / 2;

var dataset = [
    { label: 'Bob', count: 10 },
    { label: 'Sally', count: 20 },
    { label: 'Matt', count: 30 },
    { label: 'Jane', count: 40 }
];

var colorScale = d3.scaleOrdinal();
colorScale.range(d3.schemeCategory10);
colorScale.domain(dataset.map(function(element){
    return element.label;
}));

设置 SVG

下一个代码块相当标准。将以下代码添加到 app.js 的底部:

d3.select('svg')
    .attr('width', WIDTH)
    .attr('height', HEIGHT);

为每个饼图段添加路径

让我们为数据集中的每个元素添加路径元素。将以下代码添加到 app.js 的底部:

var path = d3.select('g').selectAll('path')
    .data(dataset)
    .enter()
    .append('path')
    .attr('fill', function(d) {
        return colorScale(d.label);
    });

如果我们在开发者工具中检查我们的元素,我们会看到路径已被添加,并且每个路径都有一个填充值,这是由 colorScale(d.label) 决定的,它将每个数据对象的标签映射到一个颜色:

图片

生成创建弧的函数

路径有填充颜色,但没有形状。如果你还记得,<path> 元素有一个 d= 属性,它决定了它们的绘制方式。我们想要设置某种将数据映射到 d= 字符串的方法,如下面的代码(你不需要添加下一个代码片段;它只是为了参考):

.attr('d', function(datum){
    //return path string here
})

幸运的是,D3 可以生成我们需要的匿名函数,用于上一代码片段中 .attr() 的第二个参数。将以下内容添加到 app.js 中,就在我们之前的 var path = d3.select('g').selectAll('path')... 代码之前:

var arc = d3.arc()
    .innerRadius(0) //to make this a donut graph, adjust this value
    .outerRadius(radius);

让我们将这个函数插入到我们之前的 var path = d3.select('g').selectAll('path')... 代码的正确位置(尽管现在它还不起作用):

var path = d3.select('g').selectAll('path')
    .data(dataset)
    .enter()
    .append('path')
    .attr('d', arc) //add this
    .attr('fill', function(d) {
        return colorScale(d.label);
    });

格式化用于弧的数据

我们的原因是arc()函数不会工作,因为数据格式不正确,不适合该函数。我们生成的弧函数期望数据对象具有起始角度、结束角度等。幸运的是,D3 可以重新格式化我们的数据,使其与我们的生成arc()函数兼容。为此,我们将生成一个pie函数,它将接受一个数据集,并添加必要的属性,如起始角度、结束角度等。在var path = d3.select('g').selectAll('path')...代码之前添加以下内容:

var pie = d3.pie()
    .value(function(d) { return d.count; }) //use the 'count' property each value in the original array to determine how big the piece of pie should be
    .sort(null); //don't sort the values

我们的pie变量是一个函数,它接受一个值数组作为参数,并返回一个格式化以供我们的arc函数使用的对象数组。暂时将以下代码添加到app.js底部,并在 Chrome 的开发者工具中的控制台中查看:

console.log(pie(dataset));

以下内容将被显示:

图片

你现在可以移除console.log(pie(dataset))调用。我们可以使用这个pie()函数来将数据附加到我们的路径上。调整app.js底部的var path = d3.select('g').selectAll('path')代码,如下所示:

var path = d3.select('g').selectAll('path')
    .data(pie(dataset)) //adjust this line to reformat data for arc
    .enter()
    .append('path')
    .attr('d', arc)
    .attr('fill', function(d) {
        return colorScale(d.label);
    });

不幸的是,现在,附加到我们路径元素的数据数组中的每个对象都没有.label属性,所以我们的.attr('fill', function(d) {})代码出错了。幸运的是,我们的数据确实有一个.data属性,它反映了我们在传递给pie()函数之前的数据外观。让我们调整var path = d3.select('g').selectAll('path')代码,使用以下代码,如下所示:

var path = d3.select('g').selectAll('path')
    .data(pie(dataset))
    .enter()
    .append('path')
    .attr('d', arc)
    .attr('fill', function(d) {
        return colorScale(d.data.label); //use .data property to access 
        original data
    });

到目前为止,我们的代码如下:

var WIDTH = 360;
var HEIGHT = 360;
var radius = Math.min(WIDTH, HEIGHT) / 2;

var dataset = [
    { label: 'Bob', count: 10 },
    { label: 'Sally', count: 20 },
    { label: 'Matt', count: 30 },
    { label: 'Jane', count: 40 }
];

var mapper = d3.scaleOrdinal();
var colorScale = d3.scaleOrdinal();
colorScale.range(d3.schemeCategory10);
colorScale.domain(dataset.map(function(element){
    return element.label;
}));

d3.select('svg')
    .attr('width', WIDTH)
    .attr('height', HEIGHT);

var arc = d3.arc()
    .innerRadius(0)
    .outerRadius(radius);

var pie = d3.pie()
    .value(function(d) { return d.count; })
    .sort(null);

var path = d3.select('g').selectAll('path')
    .data(pie(dataset))
    .enter()
    .append('path')
    .attr('d', arc)
    .attr('fill', function(d) {
        return colorScale(d.data.label);
    });

上述代码产生以下结果:

图片

调整派对的定位

目前,我们只能看到派对图的右下四分之一。这是因为派对从(0,0)开始,但我们可以通过调整我们的d3.select('svg')代码来移动包含派对的group元素,如下所示:

d3.select('svg')
    .attr('width', WIDTH)
    .attr('height', HEIGHT);
var container = d3.select('g') //add this line and the next:
    .attr('transform', 'translate(' + (WIDTH / 2) + ',' + (HEIGHT / 2) + ')'); //add this line

派对图现在看起来如下:

图片

制作甜甜圈图

如果你想要在派对的中心有一个洞,只需调整arc()函数的内半径,如下所示:

var arc = d3.arc()
    .innerRadius(100) //to make this a donut graph, adjust this value
    .outerRadius(radius);

图表现在将看起来如下:

图片

移除派对的某些部分

我们希望能够点击派对的某个部分来移除它。首先,让我们给数据添加 ID,以便更容易地移除。调整app.js顶部的var dataset代码:

var dataset = [
    { id: 1, label: 'Bob', count: 10 }, //add id property
    { id: 2, label: 'Sally', count: 20 }, //add id property
    { id: 3, label: 'Matt', count: 30 }, //add id property
    { id: 4, label: 'Jane', count: 40 } //add id property
];

现在,让我们在将数据映射到路径时使用这些 ID。调整app.js底部的var path = d3.select('g').selectAll('path')代码的.data()部分,如下所示:

var path = d3.select('g').selectAll('path')
    .data(pie(dataset), function(datum){ //attach datum.data.id to each element
        return datum.data.id
    })

让我们通过给每个元素添加一个_current属性来保存每个元素当前数据的记录(我们稍后会用到)。将.each(function(d) { this._current = d; });添加到app.js底部var path = d3.select('g')代码的末尾:

var path = d3.select('g').selectAll('path')
    .data(pie(dataset), function(datum){
        return datum.data.id
    })
    .enter()
    .append('path')
    .attr('d', arc)
    .attr('fill', function(d) {
        return colorScale(d.data.label);
    })//watch out! remove the semicolon here
    .each(function(d) { this._current = d; }); //add this

通过在app.js底部添加以下代码来创建点击处理程序:

path.on('click', function(clickedDatum, clickedIndex){
});

使用 JavaScript 的原生 filter 函数从数据集中移除选定的数据。调整我们刚刚添加的代码,如下所示:

path.on('click', function(clickedDatum, clickedIndex){
    dataset = dataset.filter(function(currentDatum, currentIndex){ //new
        return clickedDatum.data.id !== currentDatum.id //new
    }); //new
});

通过在我们的点击处理函数中添加以下内容,从 SVG 中移除 path 元素:

path.on('click', function(clickedDatum, clickedIndex){
    dataset = dataset.filter(function(currentDatum, currentIndex){
        return clickedDatum.data.id !== currentDatum.id
    });
    path //new
        .data(pie(dataset), function(datum){ //new
            return datum.data.id //new
        }) //new
        .exit().remove(); //new
});

现在,如果我们点击橙色部分,我们应该得到以下结果:

让我们关闭甜甜圈并添加一个过渡效果。将以下内容添加到我们的点击处理函数的底部。查看以下代码中的注释,以了解每一行的作用:

path.on('click', function(clickedDatum, clickedIndex){
    dataset = dataset.filter(function(currentDatum, currentIndex){
        return clickedDatum.data.id !== currentDatum.id
    });
    path
        .data(pie(dataset), function(datum){
            return datum.data.id
        })
        .exit().remove();

    path.transition() //create the transition
        .duration(750) //add how long the transition takes
        .attrTween('d', function(d) { //tween the d attribute
            var interpolate = d3.interpolate(this._current, d); 
            //interpolate 
            from what the d attribute was and what it is now
            this._current = interpolate(0); //save new value of data
            return function(t) { //re-run the arc function:
                return arc(interpolate(t));
            };
        });
});

现在,当我们点击橙色部分时,甜甜圈会平滑地关闭,如下所示:

摘要

在本章中,我们创建了一个饼图,当你从其中移除部分时,它会动画显示。你学习了如何从数据生成路径,这样你就可以在不直接在路径元素中指定绘图命令的情况下获取饼图的不同部分。你还学习了如何使用动画使可视化看起来更专业。最后,你学习了如何移除饼图的部分,并让其他路径元素重新绘制自己,从而使结果是一个完整的饼图。

在下一章中,我们将使用 D3 创建一个可视化数据中各个节点之间关系的图表。

第七章:使用物理创建力导向图

本章将介绍如何制作一个力导向图,以可视化各个节点之间的关系。

在本课中,你将学习以下主题:

  • 创建一个基于物理的力,使节点居中

  • 创建一个基于物理的力,使节点相互排斥

  • 创建一个基于物理的力,将节点链接起来以显示它们的关系

本节完整的代码可以在github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter07找到。

什么是力导向图?

力导向图是一种受各种力(如重力和排斥力)影响的图。在创建关系图时,它可能非常有帮助。

如何设置关系图

以下部分将概述我们将要构建的内容。概述将涵盖实现的显示方面和物理方面。

显示

显示方面控制我们看到的内容;显示将包括以下内容:

  • 一个表示人员的节点列表,以圆形的形式显示

  • 表示人员之间连接的链接列表,以线条的形式显示

物理

模拟的物理控制元素如何交互,如下所示:

  • SVG 中心的居中力将使所有节点向其移动

  • 每个节点上的排斥力将防止节点彼此靠得太近

  • 链接力将连接每个节点,以便它们不会相互排斥得太厉害

设置 HTML

我们的文件将是一个相当标准的index.html文件,但我们需要两个<g>元素,如下所示:

  • 一个用于包含节点(人员:圆形)

  • 一个用于包含链接(关系:线条)

这是我们代码应该看起来像的:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="img/d3.v5.min.js"></script>
    </head>
    <body>
        <svg>
            <g id="nodes"></g>
            <g id="links"></g>
        </svg>
        <script src="img/app.js" charset="utf-8"></script>
    </body>
</html>

为节点和链接设置样式

为我们的圆形(节点/人员)和线条(链接/关系)创建一个app.css文件,如下所示:

circle {
    fill: red;
    r: 5;
}

line {
    stroke: grey;
    stroke-width: 1;
}

不要忘记在index.html文件中创建指向它的链接,如下所示:

<head>
    <link rel="stylesheet" href="app.css">
    <script src="img/d3.v5.min.js"></script>
</head>

设置 SVG

在我们的app.js文件顶部添加以下内容:

var WIDTH = 300;
var HEIGHT = 200;

d3.select("svg")
    .attr("width", WIDTH)
    .attr("height", HEIGHT);

如果我们在 Chrome 中打开index.html并查看开发者工具中的元素,我们应该看到以下内容:

图片

添加人员的数据

让我们在app.js文件的底部创建一个人员数组,如下所示:

var nodesData =  [
    {"name": "Charlie"},
    {"name": "Mac"},
    {"name": "Dennis"},
    {"name": "Dee"},
    {"name": "Frank"},
    {"name": "Cricket"}
];

添加关系的数据

现在,让我们通过在app.js文件的底部添加以下数组来创建关系。请注意,属性必须是source和target,以便 D3 执行其魔法:

var linksData = [
    {"source": "Charlie", "target": "Mac"},
    {"source": "Dennis", "target": "Mac"},
    {"source": "Dennis", "target": "Dee"},
    {"source": "Dee", "target": "Mac"},
    {"source": "Dee", "target": "Frank"},
    {"source": "Cricket", "target": "Dee"}
];

将圆形添加到 SVG 中

将以下内容添加到app.js文件的底部:

var nodes = d3.select("#nodes")
    .selectAll("circle")
    .data(nodesData)
    .enter()
    .append("circle");

这将为nodesData数组中的每个元素创建一个圆形。我们的开发者工具应该看起来如下所示:

图片

将线条添加到 SVG 中

将以下内容添加到app.js文件的底部:

var links = d3.select("#links")
    .selectAll("line")
    .data(linksData)
    .enter()
    .append("line");

这将为我们的 linksData 数组中的每个元素创建一条线。我们的开发者工具应该如下所示:

创建一个模拟

现在,我们将在 app.js 的底部添加以下内容来生成一个模拟:

d3.forceSimulation()

注意,这仅仅创建了一个模拟;它没有指定模拟应该如何运行。让我们通过修改之前的代码行来告诉它应该作用于哪些数据,如下所示:

d3.forceSimulation()
    .nodes(nodesData) // add this line

指定模拟如何影响视觉元素

在这个阶段,我们的可视化效果仍然看起来一样,如下面的截图所示:

让我们的模拟影响我们创建的圆圈/线条,如下所示:

  • 模拟运行 ticks,它们运行得非常快。想象一下这是一系列非常快速发生的步骤,就像秒表的滴答声,但更快。

  • 每当发生一个新的 tick 时,你可以更新视觉元素。这允许我们的模拟进行动画。

  • D3 将计算并将位置数据附加到我们的常规数据上,以便我们可以使用它。

将以下内容添加到 app.js 的底部:

d3.forceSimulation()
    .nodes(nodesData)
    .on("tick", function(){
        nodes.attr("cx", function(datum) {return datum.x;})
            .attr("cy", function(datum) {return datum.y;});

        links.attr("x1", function(datum) {return datum.source.x;})
            .attr("y1", function(datum) {return datum.source.y;})
            .attr("x2", function(datum) {return datum.target.x;})
            .attr("y2", function(datum) {return datum.target.y;});
    });

现在,我们的圆圈彼此之间稍微远离一些,但这只是没有附加任何力的副作用。我们将添加力:

创建力

让我们在屏幕中心创建一个中心力,它将把所有元素拉向它。调整我们在上一步中添加的代码,使其看起来如下。注意我们只向之前的代码中添加了 .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2)):

d3.forceSimulation()
    .nodes(nodesData)
    // add the line below this comment
    .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2))             .on("tick", function(){
        nodes.attr("cx", function(datum) {return datum.x;})
            .attr("cy", function(datum) {return datum.y;});

        links.attr("x1", function(datum) {return datum.source.x;})
            .attr("y1", function(datum) {return datum.source.y;})
            .attr("x2", function(datum) {return datum.target.x;})
            .attr("y2", function(datum) {return datum.target.y;});
    });

现在,我们的圆圈被拉向 SVG 元素的中心:

在每个节点上创建一个力,使它们相互排斥。就像在上一步中一样,我们将在之前的代码中只添加 .force("charge_force", d3.forceManyBody()):

d3.forceSimulation()
    .nodes(nodesData)
    .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2))
    // add the line below this comment
    .force("charge_force", d3.forceManyBody())
    .on("tick", function(){
        nodes.attr("cx", function(datum) {return datum.x;})
            .attr("cy", function(datum) {return datum.y;});

        links.attr("x1", function(datum) {return datum.source.x;})
            .attr("y1", function(datum) {return datum.source.y;})
            .attr("x2", function(datum) {return datum.target.x;})
            .attr("y2", function(datum) {return datum.target.y;});
    });

你会注意到圆圈的 cx/cy 值最初会迅速变化,最终停止。这是因为 D3 正在运行一个模拟。注意 center_force 正在尝试与 charge_force 达到一个平衡状态。你甚至会注意到当你第一次加载页面时,圆圈会从中心向外移动。这也是由于同样的原因:

最后,我们将创建节点之间的链接,以便它们不会相互排斥得太厉害。就像在上一步中一样,我们将添加以下代码到之前的代码中:

.force("links", d3.forceLink(linksData).id(function(datum){
    return datum.name
}).distance(160))

我们最后的代码块现在应该如下所示:

d3.forceSimulation()
    .nodes(nodesData)
    .force("center_force", d3.forceCenter(WIDTH / 2, HEIGHT / 2))
    .force("charge_force", d3.forceManyBody())
    //add the three lines below this comment
    .force("links", d3.forceLink(linksData).id(function(datum){
        return datum.name
    }).distance(160))
    .on("tick", function(){
        nodes.attr("cx", function(datum) {return datum.x; })
            .attr("cy", function(datum) {return datum.y; });

        links.attr("x1", function(datum) {return datum.source.x;})
            .attr("y1", function(datum) {return datum.source.y;})
            .attr("x2", function(datum) {return datum.target.x;})
            .attr("y2", function(datum) {return datum.target.y;});
    });
  • d3.forceLink 函数接受链接数组。然后它使用每个链接数据对象的源和目标属性,通过它们的 .name 属性(如我们刚刚编写的函数的返回值中指定)将节点连接起来。

  • 你可以通过添加 .distance() 来指定每条线在视觉上有多长。

最后,我们的图表看起来如下:

图片

摘要

在本章中,我们使用了 D3 来创建一个图形,用于可视化数据中各个节点之间的关系。这在诸如绘制朋友网络、展示母公司和子公司关系或显示公司员工层级等场景中非常有用。

在第八章 映射中,我们将介绍如何从 GeoJSON 数据创建地图。

第八章:映射

D3 是生成地图的强大工具。为此,我们使用特殊格式的 JSON 数据来生成 <path> SVG 元素。这种特殊格式的 JSON 数据称为 GeoJSON,在本章中,我们将使用它来创建世界地图。

在本章中,我们将涵盖以下主题:

  • 创建地图

  • 定义 GeoJSON

  • 使用投影

  • 使用投影和 GeoJSON 数据生成 <path>

本章的完整代码可以在以下网址找到:github.com/PacktPublishing/D3.js-Quick-Start-Guide/tree/master/Chapter08.

定义 GeoJSON

GeoJSON 只是具有特定属性的 JSON 数据,这些属性被分配了特定的数据类型。以下是一个 GeoJSON 的示例:

{
    "type": "Feature",
    "geometry": {
        "type": "Point",
        "coordinates": [125.6, 10.1]
    },
    "properties": {
        "name": "Dinagat Islands"
    }
}

在这个例子中,我们有一个 Feature,其 geometry 是一个坐标为 [125.6, 10.1] 的 Point。它的名字是 Dinagat Islands。每个 Feature 都将遵循这种通用结构。以下是一个类型为 STRING 的示例:

{
    "type": STRING,
    "geometry": {
        "type": STRING,
        "coordinates": ARRAY
    },
    "properties": OBJECT
}

我们还可以有一个 FeatureCollection,它包含一个 features 数组中分组在一起的许多特征。在以下代码片段中,你可以看到一个具有不同 geometry 的 FeatureCollection 示例:

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [102.0, 0.5]
            },
            "properties": {
                "prop0": "value0"
            }
        },
        {
            "type": "Feature",
            "geometry": {
                "type": "LineString",
                "coordinates": [
                    [102.0, 0.0], [103.0, 1.0], [104.0,   
                     0.0], [105.0, 1.0]
                ]
            },
            "properties": {
                "prop0": "value0",
                "prop1": 0.0
            }
        },
        {
            "type": "Feature",
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                       [100.0, 0.0], [101.0, 0.0], 
                       [101.0, 1.0], [100.0, 1.0], 
                        [100.0, 0.0]
                    ]
                ]
            },
            "properties": {
                "prop0": "value0",
                "prop1": { "this": "that" }
            }
        }
    ]
}

这里是通用形式:

{
    "type": "FeatureCollection",
    "features": ARRAY
}

features 属性是一个之前定义的特征对象的数组。

设置 HTML

让我们设置一个基本的 D3 页面,使用以下代码:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="img/d3.v5.min.js" charset="utf-8">
    </script>
    <script src="img/map_data3.js" charset="utf-8">
    </script>
</head>
<body>
    <svg></svg>
    <script src="img/app.js" charset="utf-8"></script>
</body>
</html>

与之前章节中使用的设置相比,唯一不同的是以下行:

<script src="img/map_data3.js" charset="utf-8">
</script>

前一行只是加载一个外部 JavaScript 文件,该文件将我们的 GeoJSON 数据设置到一个变量中。代码的开始部分如下所示:

var map_json = {
    type: "FeatureCollection",
    features: [
        {
            type: "Feature",
            id: "AFG",
            properties: {
                name: "Afghanistan"
            },
            geometry: {
                type: "Polygon",
                coordinates: [
                    //lots of coordinates
                ]
            }
        }
        // lots of other countries
    ]
}

注意,map_json 变量只是一个遵循 GeoJSON 结构的 JavaScript 对象(它添加了一个可选的 idproperty)。这非常重要。如果对象不遵循 GeoJSON 结构,D3 就不会按预期工作。

在生产环境中,你可能需要通过 AJAX 调用来获取这些数据,或者至少创建自己的 GeoJSON 文件,类似于托管在 rawgit.com/ 上的文件。前面的设置是为了简化学习,降低与 AJAX 相关的复杂性。

使用投影

现在,让我们开始我们的 app.js 文件,如下所示:

var width = 960;
var height = 490;

d3.select('svg')
    .attr('width', width)
    .attr('height', height);

在 app.js 的底部,让我们添加以下代码:

var worldProjection = d3.geoEquirectangular();

这生成了一个投影,它决定了我们在平面上如何显示一个圆形的世界。我们可以使用很多不同类型的投影,这些投影可以在 github.com/d3/d3-geo/blob/master/README.md#azimuthal-projections 中看到。

前一行告诉 D3 创建一个 equirectangular 投影 (github.com/d3/d3-geo/blob/master/README.md#geoEquirectangular).

使用投影和 GeoJSON 数据生成路径

现在我们有了我们的投影,我们将为 map_json.features 数组中的每个数据元素生成 <path> 元素。然后,我们将每个元素的填充设置为 #099。将以下内容添加到 app.js 的末尾:

d3.select('svg').selectAll('path')
    .data(map_json.features)
    .enter()
    .append('path')
    .attr('fill', '#099');

以下截图显示了如果我们打开 index.html 并在开发者工具中的元素标签页中查看,它应该看起来像什么:

我们创建了路径元素,但每个元素都需要一个 d 属性,这将决定它们如何被绘制(即它们的形状)。

我们想要的是以下这样的东西:

d3.selectAll('path').attr('d', function(datum, index){
    //use datum to generate the value for the 'd' attributes
});

编写前面注释中描述的那种代码将会非常困难。幸运的是,D3 可以为我们生成整个函数。我们只需要指定之前创建的投影。在 app.js 的底部添加以下代码:

var dAttributeFunction = d3.geoPath()
    .projection(worldProjection);

d3.selectAll('path').attr('d', dAttributeFunction);

geoPath() 生成我们将用于 d 属性的函数,并且投影 (worldProjection) 告诉它使用之前创建的 worldProjection 变量,这样路径元素就会以等经纬投影的形式出现,如下所示:

摘要

在本章中,我们讨论了 GeoJSON,它的用途以及为什么它与更通用的 JSON 数据不同。我们还介绍了如何使用 D3 创建投影并将 GeoJSON 数据渲染为地图。利用这些信息,我们可以创建各种有趣的地图,如国家、城市、镇或任何我们有 GeoJSON 数据的区域。我们可以使用不同的投影以有趣的方式查看这些数据。

恭喜!你已经到达了这本书的结尾。现在,去创建令人惊叹的可视化吧。

posted @ 2025-09-26 22:08  绝不原创的飞龙  阅读(60)  评论(0)    收藏  举报