D3 4.x数据可视化实战手册(第2版)

978-7-115-49787-1
作者: [加] 朱启(Nick Zhu)
译者: 韩波
编辑: 王峰松

图书目录:

详情

本书展示了D3数据可视化库最新版本的强大功能,并通过代码示例让读者快速熟悉D3。同时,本书收录了诸多实际的数据可视化案例,有助于读者解决实际应用中的可视化问题。本书适合那些熟悉HTML、CSS和JavaScript的开发者。

图书摘要

版权信息

书名:D3 4.x数据可视化实战手册(第2版)

ISBN:978-7-115-49787-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [加] 朱启(Nick Zhu)

译    韩 波

责任编辑 王峰松

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Copyright © 2017 Packt Publishing. First published in the English language under the title Data Visualization with D3 4.x Cookbook, Second Edition, ISBN 9781786468253. All rights reserved.

本书中文简体字版由Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


当今,我们的世界已经进入万物互联的时代,每天都会产生海量的数据,如果直接面对这些数据,可能让人无从下手。相反,如果将数据可视化,用形象生动的形式展现出来,不仅有利于分析其中的关联,还能攫取可能存在的商业机会。本书旨在通过大量的示例和代码,向读者讲述如何利用D3 4.x来实现数据可视化。只要读者了解JavaScript,就能完全掌握本书的内容。

本书共13章,从如何搭建D3.js的开发环境开始,逐步介绍D3中的各种操作,其中包括选集、数据的初步处理、数据映射、坐标轴组件、动画过渡效果、SVG相关介绍、绘制图表、安排布局、可视化交互、力学模拟、制作地图和测试驱动。为了帮助读者理解这些丰富的概念,本书提供了大量的示例和代码。最后,在附录部分,为读者介绍了另外两个JavaScript库,主要是关于三维制图和多维图表的。

如果读者是一名熟悉HTML、CSS、JavaScript的开发人员或架构师,并且希望了解D3的大部分知识,那么本书将非常合适。本书还可作为资深的D3数据可视化程序开发人员的快速参考指南。


朱启(Nick Zhu)是一位专业的程序员和数据工程师,在软件开发、大数据和机器学习领域拥有十几年的实战经验。目前,他担任在线购物元搜索引擎Yroo的首席技术官,同时也是该网站的创始人之一。此外,他还是基于D3开发的、可用于制作多维图表的流行开发库dc.js的创始人。


Scott Becker是俄勒冈州波特兰一家名为Olio Apps的软件咨询公司的合伙人。

他构建过许多系统,包括地理空间数据集市场、面向医疗行业的HIPAA兼容视频服务以及数据安全产品中的违规可视化等领域。他目前供职于shoutbase网站,正在奋力打造下一代跟踪系统。此外,他还在Deveo TV上提供基于D3.js的数据可视化视频课程。


D3.js是一个JavaScript库,主要用于对数据的动态图表进行展示。利用HTML、SVG以及CSS,D3可以让数据展现得更加鲜活。借助于D3,读者可以在最终视觉效果方面获得最大的控制权。可以说,D3是当下最热门、最强大的基于网络的数据可视化技术。

D3 v4是D3库的最新版本。本书的第2版已经针对该版本进行了全面更新,以涵盖和利用D3 v4 API、模块化数据结构以及力的改良实现。本书旨在全方位指导读者迅速掌握基于D3的数据可视化技术。本书在手,读者就可以借助于其中实用的方案、插图和代码示例,快速高效地创建令人叹为观止的专业数据可视化程序。

本书由浅入深,首先介绍了一些D3数字可视化编程中的基本概念,继而通过一些代码示例逐一展示D3的其他特性。

在这里,读者将学习到数据可视化的基本概念、JavaScript的函数式编程以及D3的基础概念,例如元素选取、数据绑定、动画以及SVG生成。除此之外,读者还将领略到D3的一些高级特性,例如插值、自定义中间帧、定时器、队列、力的操作等。本书同时提供了许多预生成的图表和代码,以帮助读者更快起步。

第1章,D3.js入门指南,是D3.js的热身运动。它涵盖了一些基本概念,诸如D3.js是什么以及如何搭建一个适用于D3.js数据可视化程序的开发环境等。

第2章,精挑细选,向读者介绍了D3数据可视化中最基本的一项操作—选集。选集可以帮助读者定位页面上的元素。

第3章,与数据同行,探索了任何数据可视化程序中都会涉及的基础问题—如何通过程序构造、可视化效果来展示数据。

第4章,张弛有“度”,介绍了数据可视化中非常重要的一个子领域。作为一名数据可视化的开发人员,如何将数据映射为可视化元素,是每天都要面对的问题,本章就此问题进行了深入探索。

第5章,玩转坐标轴,介绍了坐标轴组件的用法以及基于笛卡儿坐标系的可视化技术。

第6章,优雅变换,介绍了与过渡相关的概念。“一图胜千言”正是对数据可视化的最好总结。这一章涵盖了D3库中过渡以及动画的相关概念。

第7章,形状之美,介绍了与SVG相关的概念。SVG是广泛用于数据可视化方面的W3C(World Wide Web Consortium,万维网联盟)标准。

第8章,图表美化,探索了数据可视化中最广为人知的组件——图表。图表是一种定义良好且易于理解的数据可视化展示方式。

第9章,井然有序,集中讲述了D3的布局。D3的布局是一种算法,用于计算和生成元素的位置信息,这些元素可用于生成复杂又有趣的可视化程序。

第10章,可视化交互,集中讲述了D3对可视化交互的支持。换句话说,即如何向可视化程序添加控制能力。

第11章,使用“原力”,介绍了D3中又一神奇的特性——力。力模拟是数据可视化程序中最“炫”的一项技术。

第12章,地图的奥秘,介绍了D3中基本的地图可视化技术以及如何利用D3实现一个功能完整的可视化地图。

第13章,测试驱动,帮助读者以专业TDD方式来实现可视化程序。

附录,分分钟搞定交互式分析,介绍了Crossfilter.js和dc.js技术在三维制图中的应用。

如果读者是一名熟悉HTML、CSS、JavaScript的开发人员或者架构师,并且希望了解D3的大部分知识,那么本书非常合适。本书还可以作为资深的D3数据可视化程序开发人员的快速参考指南。

在本书中,读者会发现有几个小标题(准备工作、开始编程、工作原理、更多内容、参考阅读)随处可见。

为了明确说明如何实现一个解决方案,我们将用到下列小标题。

本小节将说明解决方案要实现的目标,以及如何完成解决方案所需软件或背景的相关设置。

本小节介绍实现解决方案的具体步骤。

本小节通常对上一节中发生的情况进行详细说明。

本小节提供与解决方案有关的附加信息,以加深读者的理解。

本小节提供与解决方案有关的其他有用信息的链接。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit.com/ selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

异步社区”是人民邮电出版社旗下信息技术专业图书社区,致力于出版精品信息技术图书和相关学习产品,为作(译)者提供优质出版服务。异步社区创办于2015年8月,提供大量精品信息技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

异步图书”是由异步社区编辑团队策划出版的精品信息技术专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的标志。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。

异步社区

微信服务号


本章涵盖以下内容:

本章旨在帮助读者迅速上手D3.js。我们将为读者介绍一些基本知识,比如什么是D3.js,如何搭建典型的D3.js数据可视化环境。此外,还有专门的部分解释了JavaScript中一些比较冷门但对于D3.js来说又是非常重要的特性。

什么是D3?D3是指数据驱动文档,其官方定义如下所示。

D3.js(Data-Driven Documents)是一个JavaScript库,它可以通过Web标准来实现数据的可视化。D3可以利用HTML、SVG和Canvas把数据鲜活形象地展现出来。由于它同时提供了强大的可视化和交互技术,可以让使用者以数据驱动的方式去操作DOM,因而可以让使用者的程序轻松兼容现代主流浏览器,随心所欲地为数据设计合适的可视化接口。

——D3 GitHub维基(2016年8月)

从某种意义上讲,D3是这样一个特殊的JavaScript库:它利用现有的Web标准,通过更简单的(数据驱动)方式来实现令人惊艳的可视化效果。D3.js由Mike Bostock创建。之前,Bostock还创建过一个叫Protovis的数据可视化JavaScript库,如今它已经被D3.js取代。如果希望了解更多诸如D3.js制作过程、影响Protovis和D3.js的相关理论这类的信息,可以访问后面给出的链接。本书将着重介绍如何使用D3.js来提升可视化效果。由于D3使用JavaScript实现数据可视化的方式比较特别,因此刚开始接触它时,可能会让人觉得有些不太适应。我希望通过本书中的大量主题,其中有简单的,也有高级的,来帮助大家更好更高效地使用D3。一旦正确理解了这些主题,就能借助D3让数据可视化的效率和丰富程度产生指数化的增长。

 

D3背后主旨思想更为正式的介绍,可以参考Mike Bostock于2010年在IEEE InfoVis发表的论文Declarative Language Design for Interactive Visualization

如果对于D3是怎么来的感兴趣,建议阅读Mike Bostock于2011年在IEEE InfoVis发表的论文D3: Data-Driven Document

Protovis,即D3.js的前身,是Mike Bostock和斯坦福可视化组的Jeff Heer共同创建的。

在开始使用D3开发数据可视化项目之前,需要搭建一个相应的开发环境。在本节中,我们将介绍如何在几分钟内迅速搭建一个简单的D3开发环境。

在开始之前,你应确保已经选好了自己的文本编辑器,并且正确安装到了自己的计算机上。

首先,需要下载D3.js。

1.可以从https://github. com/mbostock/d3/tags下载d3.js的各个版本。另外,如果对开发中的最新D3版本感兴趣,可以fork代码库https://github.com/mbostock/d3。

2.下载并且解压缩后,在提取的文件夹中可以看到d3.js和d3.min.js两个JavaScript文件以及其他信息文件。在开发过程中,最好使用d3.js而非使用最小化版本,因为前者可以帮你深入到D3库中跟踪调试JavaScript代码。此后,把d3.js和包含下列HTML代码的index.html放在同一个文件夹里。

<!-- index.html -->
 <!DOCTYPE html>
 <html>
 <head>
     <meta charset=""utf-8"">
     <title>Simple D3 Dev Env</title>
     <script type=""text/javascript"" src=""d3.js""></script>
 </head>
 <body>

 </body>
 </html>

这样,一个最简单的D3数据可视化开发环境就搭建好了。接下来读者就可以用自己最喜欢的文本编辑器打开那个html文件,开启开发之旅,然后用浏览器打开它来查看可视化的效果。

 

读者可以从网址https://github.com/NickQiZhu/d3-cookbook- v2/tree/master/ src/chapter1/simple-dev-env下载这个例子的源码。

D3是个相当独立的JavaScript程序库。除了浏览器已经提供的JavaScript库之外,它不依赖于任何其他JavaScript库。

 

如果用于展示数据的目标浏览器环境涉及IE9,建议使用Aight兼容库和Sizzle selector engine。前者下载地址为https:// github.com/shawnbot/aight,后者下载地址为http://sizzlejs.com/。

在D3 v4发布之前,在头部信息中包含以下字符编码指令是至关重要的,因为D3的旧版本在其源中使用UTF-8字符(如π)。如果使用D3 v4.x,则不再需要它们了。然而,这样做仍不失为明智之举,因为读者包含的其他JavaScript库仍有可能使用UTF-8符号,如下例所示:

<meta charset=""utf-8"">

 

D3是完全开源的。这个库使用了其作者Mike Bostock自己定制的开源许可证。该许可证与流行的MIT许可证十分类似,唯一不同之处在于,它明确声明了Mike Bostock的名字未经允许不可用作此软件的派生品的标识,或者用以扩大此软件的派生品的影响。

本书提供了大量的代码示例。所有的示例源码均托管于流行的开源社区代码托管平台GitHub上,读者可通过https://github.com访问该网站并下载相应的代码。

如何获取源码

最简单的方式就是直接克隆本书的Git代码库(https://github.com.NickQiZhu/ d3- cookbook),从而获取所有示例代码。如果不打算为这些示例代码搭建开发环境,跳过这步即可。

 

如果不熟悉Git也不要紧,它的克隆(clone)概念很类似于其他的版本控制软件中的签出(checkout)。不过,克隆所做的并非简单地签出文件,而是把所有分支和历史复制到了读者的本地计算机,也就是把整个代码库都复制到了本地计算机中,所以读者完全可以在本地环境中离线使用这个复制过来的代码库。

首先,读者需要在自己的计算机上安装Git客户端,该客户端的下载列表地址为http://git- scm.com/ downloads。此外,读者还可以从http://git-scm.com/book/ en/Getting-Started- Installing-Git找到针对不同操作系统的安装说明。

 

另一个使用Git和GitHub的流行方式是安装GitHub客户端,它提供了比Git更丰富的功能。不过,在作者编写本书时,GitHub仅提供了Windows版和Mac OS版的客户端软件,下载地址为https://desktop.github. com/。

一旦安装好了Git客户端,键入以下命令即可将所有的示例代码下载到自己的计算机上:

> git clone git@github.com:NickQiZhu/d3-cookbook-v2.git

前面搭建的简易环境已经足以处理本书中的大部分示例。但是,如果读者正开发一个略复杂的数据展示项目,并且需要用到大量JavaScript库的话,那么本书之前讨论的那个简单的解决方案可能就显得有些捉襟见肘了。本书将展示一个使用NPM(Node Packaged Modules,即为JavaScript库的代码库管理系统)的更加强大的开发环境。如果读者希望更快地尝试本书最丰盛的部分——各种示例代码,那么完全可以跳过这部分,直到搭建产品开发环境的时候,再回来看这部分也不迟。

首先,要确保NPM已经安装好。在安装Node.js时,NPM作为其中一部分也安装了。读者可以从http://nodejs.org/下载Node.js。选择适合自己操作系统的Node.js,安装完毕后,在终端窗口运行如下npm命令:

>  npm -v
2.15.8

如果前面的命令输出了NPM客户端的版本号,则表明安装成功。

安装完NPM后,即可创建一个包描述符文件,以便将一些手动安装过程自动化。

1.首先,在工程文件夹下创建一个名为package.json的文件,其中代码如下所示:

{
   ""name": "d3-project-template",
   ""version": "0.1.0",
   "description": "Ready to go d3 data visualization project template",
   "keywords": [
     "data visualization",
     "d3"
   ],
   "homepage": "<project home page>",
   "author": {
     "name": "<your name>",
     "url": "<your url>"
   },
   "repository": {
     "type": "git",
     "url": "<source repo url>"
   },
   "dependencies": {
       "d3":"4.x"
   },
   "devDependencies": {
       "uglify-js": "2.x"
   }
}

2.定义package.json文件后运行下面的命令:

>  npm install

 

D3 v4.x具有高度的模块化特点,所以如果工程中只需要一部分D3库,那么也可以选择性地包含D3子模块来作为依赖库。例如,如果工程中只需要d3-selection模块,那么可以在package.json文件中使用如下所示的依赖项声明:

package.json文件中的绝大部分字段仅用于提供信息,比如name、description、homepage、author和repository等。如果打算将来把自己的库发布到NPM的代码库中,那么就要用到name和version字段。

不过就目前来说,我们真正关心的是dependencies和devDependencies字段。

"dependencies": {
      "d3-selection":"1.x"
    }

 

D3是个自包容的库,运行时对外部是零依赖。然而这并不意味着它不能与其他流行的JavaScript库相互协作。作者平时也结合一些其他的库与D3搭配使用,以便让自己的工作容易些,这些库包括JQuery、Zepto.js、Underscore.js和Backbone.js等。

执行npm install命令可以自动触发NPM下载工程中所引用的所有依赖项,包括递归的下载依赖项的依赖项。所有的依赖库文件都下载到node_modules文件夹中,该文件夹位于工程文件夹中的根目录里。这些工作完成以后,只需创建一个HTML文件(与我们之前创建的那个一样),就可以直接从node_modules/d3/build/d3.js加载D3的JavaScript库。

本节的源代码可以从下列地址下载,其中包含了自动构建脚本:

https://github.com/NickQiZhu/d3-cookbook-v2/tree/master/src/chapter1/  
npm-dev-env。

工程中会有一些麻烦的地方,比如手动下载JavaScript库以时刻保持版本为最新。为了避免这些麻烦,使用NPM是行之有效的方式。当然,一些聪明的读者可能已经发现,使用这个方法可以把我们“搭建环境”的过程直接提升一个档次。想象一下,你正在构建一个大型的可视化工程,其中包含了上千行的JavaScript代码,很明显我们这里所描述的简单的搭建方式满足不了这种情形。因为“模块化的JavaScript开发”这个话题足够写一本书了,所以这里就不再讨论这方面的话题,我们将把注意力放在数据可视化和D3上。在后面单元测试的章节中,我们将针对这个话题多讲一些,演示一下可以在某些方面增强开发环境的功能,以便运行自动化单元测试。

前面提到过,读者可以通过浏览器直接打开HTML文件来查看可视化的结果,不过这种方式有一些局限性。当需要从其他数据文件中加载数据(后面的章节中就要这么做了,并且读者平时工作中也经常遇到类似的情形)时,由于浏览器内建的安全机制,这种方式就行不通了。为了绕开这个安全限制,强烈建议搭建本地的HTTP服务器,使用该服务器来维护HTML页面和数据文件,而非直接从本地文件系统加载。

搭建本地HTTP服务器

由于使用的操作系统不同,HTTP服务器的软件包不同,搭建HTTP服务器的方法也很不同。这里只介绍几种流行的搭建方式。

Python简易HTTP服务器

在进行项目开发和快速构建原型的时候,这是我最喜欢的方式。如果读者的计算机上已经安装了Python,通常UNIX/Linux/Mac OS发行版上都有,那么可以直接运行下面的命令:

>  python -m SimpleHTTPServer 8888

此外,如果读者使用的是Python 3,那么应运行如下所示的命令:

> python -m http.server 8888

这个Python小程序可以启动HTTP服务器,然后读者就可以访问该程序所在文件夹中的所有文件了。这是目前所有操作系统中运行HTTP服务器最简单的方式。

 

如果你的计算机没有安装Python,可以从http://www. python.org/getit/下载。现在所有的操作系统(诸如Windows、Linux,还有Mac),都支持Python。

Node.js HTTP服务器

安装Node.js之后(前面所做的搭建开发环境练习中包含了相应的内容),就可以轻松安装http-server模块了。与Python简易HTTP服务器类似,通过该模块,读者可以利用任意的文件夹快速启动轻量级的HTTP服务器。

首先,需要安装http-server模块,具体命令如下所示:

> npm install http-server -g

上面命令中的-g参数会把http-server模块设置为全局模块,这样就可以在命令行里直接使用http-server命令。完成此步后,可以通过下面的命令在任意文件夹内启动服务器:

> http-server -p 8888

该命令可以启动由Node.js驱动的HTTP服务器,默认端口号是8080。如果需要,也可以用 -p参数指定一个端口号。

 

如果是在Linux、UNIX、Mac等操作系统中运行该命令,则需要用sudo模式或者root权限才能使用全局安装选项-g。

那些习惯了过程式或者面向对象式的JavaScript风格的人,会感觉对D3使用函数式的JavaScript编程风格有一些不适应。本节会涵盖一些JavaScript中函数式编程最根本的概念,以便对D3有个基本的了解,将来可以用D3的风格来编写可视化工程代码。

在浏览器中打开下面文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter1/  
functional-js.html。

现在进一步了解JavaScript函数式方面的内容。请看下面的代码段:

function SimpleWidget(spec) {
  var instance = {}; // <-- A

  var headline, description; // <-- B

  instance.render = function () {
    var div = d3.select('body').append("div");

    div.append("h3").text(headline); // <-- C

    div.attr("class", "box")
    .attr("style", "color:" + spec.color) // <-- D
      .append("p")
      .text(description); // <-- E

    return instance; // <-- F
  };
  instance.headline = function (h) {
    if (!arguments.length) return headline; // <-- G
    headline = h;
    return instance; // <-- H
  };

  instance.description = function (d) {
    if (!arguments.length) return description;
    description = d;
    return instance;
  };

  return instance; // <-- I
}

  var widget = SimpleWidget({color: "#6495ed"})
    .headline("Simple Widget")
    .description("This is a simple widget demonstrating
      functional       javascript.");
  widget.render();

这段代码在页面上生成了如图1-1所示的内容。

图1-1 生成的页面内容

 

对于有Java和C#编程背景的读者,需要再提醒一下,JavaScript没有实现块作用域(block scoping)。我们这里描述的静态作用域规则,仅适用于函数/对象级别,不适用于块级别,具体如下面的代码所示。

对于上面这段代码,读者可能觉得它会打印20个数字。其实在JavaScript里,这段代码会陷入无限循环。因为JavaScript没有实现块级别的作用域,所以里面那层循环的i与外面那层循环的i是同一个变量。于是里面的循环改变了i的值,导致外面的循环永远不会结束。

尽管这段代码非常简单,但是不可否认,它与D3风格的JavaScript非常相似。这不是巧合,在JavaScript编程范型中,这叫作函数式对象。与很多有趣的话题一样,这个话题也能写一本书。不过在本节中,我会尝试尽量多讲一些这种特殊范型最重要和最常用的知识,以让不理解D3语法的读者也能创建这种风格的库文件。正如D3的维基页面上所讲的那样,这种函数式编程风格给D3带来了极大的便利。

D3的函数风格,使得多种组件插件之间的代码重用成为现实。

for(var i = 0; i < 10; i++){
  for(var i = 0; i < 2; i++){
    console.log(i);
  }
}

——D3维基(2016年8月)

函数即对象

JavaScript中的函数是对象。与其他对象一样,函数对象只是键值对的集合。函数对象与普通对象的区别就是,函数可以执行,而且带有两个隐藏的属性,即函数上下文和函数代码。这两个隐藏属性有时候会给你一个大大的“意外惊喜”,如果你有着很深的过程式编程背景,这点可能更明显。不过这也是我们格外需要注意的地方:要了解D3使用函数的奇怪方式。

 

在采纳ECMAScript语言规范第6版之前,JavaScript的大部分特性显得有些不够“面向对象”,不过在函数对象这方面,JavaScript与其他语言相比较应该更胜一筹。

现在我们心里有了相关的概念,那就再看一遍这段代码。

var instance = {}; // <-- A

var headline, description; // <-- B

instance.render = function () {
  var div = d3.select('body').append("div");

  div.append("h3").text(headline); // <-- C

  div.attr("class", "box")
    .attr("style", "color:" + spec.color) // <-- D
    .append("p")
    .text(description); // <-- E

  return instance; // <-- F
};

在第A、B和C行,可以看到instance、headline和description都是SimpleWidget这个函数对象的内部私有变量。可是render函数却是instance对象的一个方法,并且定义为对象字面量。函数本身也是对象,可以存储在对象/函数、其他变量、数组里,也可以用作函数参数。运行SimpleWidget的结果就是第I行所写的,返回一个instance对象。

function SimpleWidget(spec) {
...
  return instance; // <-- I
}

 

render函数中用到了一些我们还没讲过的D3中的函数,不过现在先不管它们,后面的章节中将详细讲解。它们也只是渲染了一些可视化的东西,与我们目前的话题没有多大的关系。

静态变量作用域

好奇的读者可能会问,这个示例中的变量作用域到底是怎样的?看上去好奇怪,render函数不仅访问了instance、headline和description,而且还访问了从SimpleWidget传进来的spec变量。这个怪异的变量作用域其实是由一个简单的静态作用域规则来决定的。可以把这个规则想象成这样:当查找一个变量引用时,先把该变量当成是一个本地变量。如果没有找到变量声明(比如第C行中的headline),就继续在父对象里找(本例中的SimpleWidget函数就是静态的父对象,headline变量的声明在第B行)。如果还是没有找到,就不断地重复这个过程,递归地去父对象里查找,一直到全局变量的定义那层。如果最后还是没有找到,就针对该变量生成引用错误。这样的作用域行为与大多数流行语言(诸如Java、C#)中的变量处理方式大相径庭,可能需要一段时间来适应,要是觉得不习惯,也不用担心,练得多了,就习惯了。

与流行的原型编程中的伪类模式相比,这样的模式通常称作函数模式。函数模式的优点是它提供了更好的信息隐藏和封装。因为只能通过静态作用域规则限定的那些嵌套定义函数来访问私有变量(示例中的headline和description),所以SimpleWidget函数返回的对象就更加灵活,也更加健壮。

如果用函数式风格创建对象,并且该对象所有的方法都没有用this,那这个对象就是持久(durable)的。持久对象就是许多功能行为的集合。

——D. Crockfort(2008年)

可变参数函数

读者看看下面的代码,就会在第G行发现一些奇怪的东西。

instance.headline = function (h) {
  if (!arguments.length) return headline; // <-- G
  headline = h;
  return instance; // <-- H
};

有读者可能会问,第G行的arguments是从哪里来的?在这段示例代码中从来没有定义过它。其实这个arguments是内建的隐藏参数,并可在函数执行时直接使用。arguments是一个数组,它保存了所在函数的全部参数。

 

实际上,arguments本身并不是JavaScript的数组对象。虽然它有length属性,并可以用索引下标访问每个元素,但是它没有JavaScript中数组对象的那么多方法(比如slice、concat)。如果要在arguments上使用JavaScript数组对象的标准方法,那么需要通过下列方式进行引用:

var newArgs = Array.prototype.slice.apply(arguments);

把这个隐藏的参数与JavaScript可以在函数声明时省略参数的功能结合起来,就可以写出instance.headline这种不需要指定参数个数的函数。在本例中,可以传参数h,也可以不传。因为如果没有传进来参数,arguments.length就返回0,headline函数就返回h;如果h有值,那么它就变成了赋值操作。为了说明清楚,我们看看下面这段代码。

var widget = SimpleWidget({color: "#6495ed"})
    .headline("Simple Widget"); // set headline
console.log(widget.headline()); // prints "Simple Widget"

可以看到,headline在参数不同的情况下,可以分别作为setter和getter(赋值操作和取值操作)。

函数级联调用

这个例子的另一个有趣地方是函数的级联调用。这也是D3库提供的一个主要的函数调用方式,因为D3库中的大多数函数设计成了这种链式的结构,以便能提供简洁、上下文连贯的编程接口。如果读者理解可变参数函数的概念,就很好理解这个了。可变参数函数(比如headline函数)能同时作为setter和getter,当其作为setter时,返回instance对象,这就使得读者可以在返回的instance上立即执行另一个函数,这就是所谓的链式调用。

看下面这段代码。

var widget = SimpleWidget({color: "#6495ed"})
  .headline("Simple Widget")
  .description("This is ...")
  .render();

在这个例子中,SimpleWidget函数返回了instance对象(如第I行所示)。

function SimpleWidget(spec) {
...
    instance.headline = function (h) {
        if (!arguments.length) return headline; // <-- G
        headline = h;
       return instance; // <-- H
    };
...
    return instance; // <-- I
}

因为headline函数在这里就是setter,同时也返回instance对象(如第H行所示)。所以,description函数可以根据其返回值直接引用,执行后也返回instance对象。最后调用了render函数。

现在我们已经大概了解了JavaScript的函数式风格,并有了可工作的D3开发环境,也准备好了使用D3提供的丰富功能来一试身手。在开始前,我还想讲几个比较重要的事情,即如何寻找、分享代码以及遇到困难时如何获取帮助。

先看几个有用的东西。

寻找、分享代码

在D3众多值得称赞的亮点中,有一个是它比其他可视化工具提供了更加丰富的示例和教程,读者可以从中汲取灵感。当我创建自己的开源可视化图表项目以及写作本书的时候,我在那些资源中获得了大量的灵感。我会在那些最棒的例子里,整理出一份清单出来。这份清单虽然不是百科全书,但却是个不错的入门参考。

如何获取帮助

即便有了这些例子、教程以及本书,读者在实践的过程里仍然会遇到问题。不过D3有数目庞大并且非常活跃的用户社区。一般情况,简单地搜索一下,就能找到满意的答案。要是没有也不用担心,D3还有强大的社区支持。


本章涵盖以下内容:

选集(selection)是基于D3的可视化项目的重要基础之一,用来定位页面上的特定视觉元素。如果读者已经熟知W3C的标准CSS选择器,或一些流行的JavaScript库(如jquery或Zepto.js)提供的选择器API,那么掌握D3的选择器API将易如反掌。不过,即便从未接触过选择器也无妨,本章将借助一些生动的例子,带领读者一步步地进入选择器的世界。这些例子涵盖了可视化中的绝大多数应用场景。

所有的现代浏览器都内嵌支持W3C的标准选择器API。然而,在网络开发,尤其是在数据可视化领域的开发中,这些API仍然具有局限性。它们只提供选择器,而并不提供集合类型。也就是说,虽然选择器API有助于在文档中选择元素,然而为了操作这些元素,读者仍然需要遍历每个元素,如以下代码段所示。

var selector = document.querySelectorAll("p");
selector.forEach(function(p){
    // do something with each element selected
    console.log(p);
});

上面的代码先选取了文档中所有的p元素,然后迭代遍历每个元素并进行相应操作。而在可视化项目中,我们需要不断地对页面上不同元素进行类似操作,这将很快演变成为单调的重复性工作。为了减少开发中的琐碎工作,D3引入了自己的选择器API。本章接下来将详细介绍D3的API的工作原理以及它具有哪些出色的特性。

在深入D3的选择器API之前,需要先介绍W3C的3级选择器API。如果读者已经掌握了这部分内容,可以跳过本节。D3的选择器API基于3级选择器(也称CSS3选择器)实现。在本节,我们先来了解一些常用的CSS3选择器语法,这些是理解D3的选择器API的基础。下面的列表给出了在数据可视化项目中最常见的CSS3选择器的习惯用法。

<div id="foo">
<foo>
<div class="foo">
<div foo="goo">
<foo><goo></foo>
<foo id="goo">
<foo class="goo">
<foo> // <-- this one
<foo>
<foo>
<foo>
<foo>//<--foo:nth-child(2)
<foo>//<--foo:nth-child(3)

CSS3选择器是一个复杂的话题,这里只列出有助于理解和高效掌握D3的一些最常用选择器,更多信息可访问W3C第3级选择器API官方文档。

 

如果目标浏览器因版本太低而不支持选择器,则可以尝试在引入D3之前,先引用Sizzle来解决向下兼容的问题。

目前W3C的下一代4级选择器API仍然处于草稿阶段,读者可以通过https://drafts.csswg.org/selectors-4/对它将提供的特性和目前的状态进行预览。

一些主要的浏览器公司已经着手实现4级选择器,如果读者对自己的浏览器当前支持哪个级别的选择器感兴趣,可以参考检测站点https://css4-selectors.com/ browser-selector- test。

在进行视觉处理时,常常需要选择页面上的单个元素。本例将展示在D3中如何使用CSS选择器来选取单个元素。

请在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
single-selection.html。

选取一些元素(比如paragraph元素)并在屏幕上输出经典的“Hello world”信息。

<p id="target"></p><!-- A -->

<script type="text/javascript">
    d3.select("p#target") // <-- B
    .text("Hello world!"); // <-- C
</script>

本例将在屏幕上显示文本“Hello world!”。

在D3中,我们用d3.select方法来选取单个元素。该select方法使用CSS3选择器字符串或者操作对象的引用作为参数,并返回D3选集。随后,用级联修饰函数对该选集的属性、内容以及HTML进行操作。这个选择器也可以用来选择多个元素,但是最终只返回选集中第一个匹配的元素。

 

本例中,在B行通过id的值选取了段落元素,然后在C行中将它的文本内容设置为“Hello world!”。所有的D3选集都支持一系列标准修饰函数,本例中用到的text函数就是其中之一。下面列出了本书中用到的部分常见的修饰函数。

// set foo attribute to goo on p element
d3.select("p").attr("foo", "goo");
//get foo attribute on p element
d3.select("p").attr("foo");
// test to see if p element has CSS class goo
d3.select("p").classed("goo");
// add CSS class goo to p element
d3.select("p").classed("goo", true);
// remove CSS class goo from p element. classed function
// also accepts a function as the value so the decision
// of adding and removing can be made dynamically
d3.select("p").classed("goo", function(){return false;});
// get p element's style for font-size
d3.select("p").style("font-size");
// set font-size style for p to 10px
d3.select("p").style("font-size", "10px");
// set font-size style for p to the result of some
// calculation. style function also accepts a function as
// the value can be produced dynamically
d3.select("p").style("font-size", function(){
    return parseFloat(d3.select(this).style('font-size')) +
            10 + 'px';
});
// get p element's text content
d3.select("p").text();
// set p element's text content to "Hello"
d3.select("p").text("Hello");
// text function also accepts a function as the value,
// thus allowing setting text content to some dynamically
// produced content
d3.select("p").text(function(){
   return Date();
});
// get p element's inner html content
d3.select("p").html();
// set p element's inner html content to "<b>Hello</b>"
d3.select("p").html("<b>Hello</b>");
// html function also accepts a function as the value,
// thus allowing setting html content to some dynamically
// produced message
d3.select("p").html(function(){
  return d3.select(this).text() +
    " <span style='color: blue;'>D3.js</span>";
});

这些修饰函数可用于单个元素以及多个元素,当应用于多元素选集时,这些函数会依次作用于每个元素。在后续的内容中将会看到类似示例。

当函数作为参数传入修饰函数时,其实同时还有一些其他的内置参数传入,从而最终实现了数据驱动计算。D3的强大之处就在于数据驱动的方式,它的名称数据驱动文档(data-driven document),也正是来自于此。本书的后续章节中将详细讨论这一主题。

通常情况下,我们很少只选取单个元素。相反,大多数情况下是同时对页面上的多个元素进行特定处理。在下面的例子中,我们将介绍D3的多元素选择器及其API。

在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
    multiple-selection.html。

本例展示了d3.selectAll函数的用途。在下面的示例代码中,我们将选取3个不同的div元素,然后利用CSS的class同时为其添加某些效果。

<div></div>
<div></div>
<div></div>

<script type="text/javascript">
    d3.selectAll("div") // <-- A
    .attr("class", "red box"); // <-- B
</script>

本例的效果如图2-1所示。

图2-1 选取多个元素

有读者可能会注意到,在上面例子中D3选集API的用法和单个元素选择器的用法非常类似。这正是D3选择器的强大之处。无论处理多少个元素,修饰函数都是不变的。我们前面提到的所有修饰函数都能够直接应用于多元素选集,也就是说,D3选集是基于集合的。

尽管上面的示例表达的意思已经足够清晰,但我们仍然要详细分析一下。在第A行中,d3.selectAll方法选取了页面上的所有div元素,该方法返回一个包含3个div元素的选集对象。然后,在第B行中,我们对这一选集调用attr函数,将这3个元素的class属性都设置为red box。从本例可见,选择和操作相关代码都非常统一,即便页面出现更多的div元素,也不用改变原有代码。尽管在这里看来它无足轻重,但在后面的章节中,我们将更深地体会到这种方式将使代码变得更加简单且易于维护。

有些时候,我们需要遍历选集中的所有元素,再根据它们的不同位置分别进行不同的操作。本节将为读者介绍如何通过D3的选集迭代API来实现这种处理。

在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
    selectioniteration.html。

D3为其选集对象提供了简单的迭代接口,我们可以用类似JavaScript数组的方式迭代D3选集。在本例中,我们将对上个例子返回选集中的3个div元素进行迭代访问,并用索引号标识每个元素。

<div></div>
<div></div>
<div></div>

<script type="text/javascript">
d3.selectAll("div") // <-- A
            .attr("class", "red box") // <-- B
            .each(function (d, i) { // <-- C
                d3.select(this).append("h1").text(i); // <-- D
            });
</script>

上述代码段将产生如图2-2所示的视觉效果。

图2-2 迭代选集中的元素

这个例子基于之前的示例,除了在第A行选取页面所有div元素,并在第B行上设置class属性之外,我们还对选集调用each函数。这说明对于多元素选集可以进行迭代,并且分别处理每一个元素。

 

这种在一个函数返回结果基础上调用另一个函数的方式称为函数级联调用(Function chaining)。如果希望进一步了解这种调用模式,可参见第1章,那里曾经对这种模式进行了介绍。

下面开始介绍selecteach和append函数。

d3.selectAll("div") // <-- A
    .attr("class", "red box") // <-- B
    .each(function (d, i) { // <-- C
        d3.select(this).append("h1").text(i); // <-- D
    });

第C行定义了一个参数为d、i的迭代函数。第D行则更加有趣,在开头部分,d3.select函数将this封装为一个d3选集,这个选集是一个用变量this表示的且包含当前DOM元素的单元素选集。这样一来,标准的D3选集API就可用在d3.select(this)上。随后,我们在当前元素选集上调用append("h1")函数,新建h1元素附加到当前元素上。然后将当前每一个新建h1元素的内容绘制为其索引值。最终效果如图2-2所示。需要注意的是,这里的索引值也是从0开始依次递增的。

 

DOM元素对象本身提供了许多接口。如果要知道在迭代函数中能够对DOM元素进行何种操作,可参见DOM元素API。

在进行可视化的时候,常常需要在特定范围下选择元素。例如,选取某个section元素下的所有div元素。在本例中,我们将介绍在D3中这种需求的不同实现方式及各自的优缺点。

在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
sub-selection.html。

下面的代码通过D3提供的两种不同方式选取了两个div元素。

<section id="section1">
    <div>
        <p>blue box</p>
    </div>
</section>
<section id="section2">
    <div>
        <p>red box</p>
    </div>
</section>

<script type="text/javascript">
    d3.select("#section1 > div") // <-- A
            .attr("class", "blue box");

    d3.select("#section2") // <-- B
            .select("div") // <-- C
            .attr("class", "red box");
</script>

代码的视觉效果如图2-3所示。

图2-3 子选择器

尽管视觉效果相同,但是这个例子中使用了两种完全不同的子选择技术。我们在这里将分别讨论它们的优缺点以及各自的适用场景。

<div>
<span>
The quick <em>red</em> fox jumps over the lazy brown dog
    </span>
</div>

如果使用下面的选择器:

div em

由于div是em元素的祖先,em是div元素的后代,所以这个例子选取了其中的em元素。

span > em

这将选取出em元素,因为在本例中em是span元素的一级子元素。而div>em将不会返回任何有效的选集,因为em并不是div的直接子元素。

 

3级选择器也支持相邻选择器,但由于它用得比较少,所以我们先略过不讲。感兴趣的读者可以参考W3C 3级选择器文档。

W3C 4级选择器还提供了许多有趣的连接符,如相邻后续(following-sibling)连接符或引用连接符,这些连接符同样功能非常强大。

这种子选择方式的好处在于父元素是先独立选取的,因此可以在继续选择子元素之前进行相应的处理。具体如以下代码所示。

d3.select("#section2") // <-- B
    .style("font-size", "2em") // <-- B-1
    .select("div") // <-- C
    .attr("class", "red box");

从以上代码可以看到,在选择div元素之前,在第B-1行中对#section2使用了一个修饰函数。我们在下一节中将进一步探索这种灵活性。

到现在为止,我们看到的D3 API都体现了函数级联调用的思想,因此它接近于形成了一个可以动态构建HTML/SVG的领域特定语言(Domain Specific Language)。在接下来的例子中,我们将看到如何只使用D3来生成前一个例子的页面结构。

 

如果对DSL不熟悉,则推荐阅读Martin Fowler在《领域特定语言》(Domain-Specific Languages)一书中的相关解释。

在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
function-chain.html。

接下来我们将展示如何用简洁且可读性更高的函数级联调用来生成动态图形。

<script type="text/javascript">
  var body = d3.select("body"); // <-- A

  body.append("section") // <-- B
      .attr("id", "section1") // <-- C
    .append("div") // <-- D
      .attr("class", "blue box") // <-- E
    .append("p") // <-- F
      .text("dynamic blue box"); // <-- G

  body.append("section")
      .attr("id", "section2")
    .append("div")
      .attr("class", "red box")
    .append("p")
      .text("dynamic red box");
</script>

上述代码生成图2-4所示的视觉效果(与之前章节效果类似)。

图2-4 函数级联调用

尽管与之前的效果很类似,但本例对DOM元素的构造过程却完全不同。如代码所示,在本例中页面上并没有任何静态HTML元素,而之前的例子中,section和div元素都是事先存在的。

下面进一步研究这些元素是如何动态创建的。在第A行中,我们先选取了顶层body元素,然后用一个临时变量body来缓存该选集结果。而后第B行在body元素内追加一个新的元素section。由于append函数返回了一个包含新添加元素的选集,因此在第C行中就可以为这个新创建的section元素的id属性赋值,这里它的值为section1。第D行为#section1附加了一个新创建的div元素,并且在第E行中设置css class为blue box。随后,类似地,我们在第F行中往这个div元素上附加一个段落元素,并在第G行中设置其文本内容为dynamic blue box。

如上所述,这种级联处理可以继续生成任意复杂的结构。事实上,典型的基于D3的数据可视化结构正是这样创建的。许多可视化项目只简单包含一个HTML骨架,然后用D3来创建剩余部分。如果希望熟练运用D3库,那么掌握这种函数级联调用的方式是必不可少的。

 

一些D3修饰函数会返回一个新的选集,例如select、append、insert函数。建议用缩进来区别应用于不同选集上的级联函数,这是个不错的做法。

虽然不常使用,但某些时候,获取D3的原始选集数组对于开发是有利的,因为无论是为了调试,还是与其他JavaScript库集成,都可能需要原始的DOM元素。在本例中,我们将对此进行展示。同时,我们也会观察D3选集对象的内部结构。

在浏览器中打开如下文件的本地副本:

https://github.com/NickQiZhu/d3-cookbook-v2/blob/master/src/chapter2/  
raw-selection.html。

当然,可以使用nth-child选择器,或者在each函数基础上使用选集迭代函数,但是在有些情况下,这些方式过于繁琐。这里提供一种处理原始选集数组更加便利的方法。在本例中,可以看到对原始选集数组进行存取和处理的方法。

<table class="table">
    <thead>
    <tr>
        <th>Time</th>
        <th>Type</th>
        <th>Amount</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>10:22</td>
        <td>Purchase</td>
        <td>$10.00</td>
    </tr>
    <tr>
        <td>12:12</td>
        <td>Purchase</td>
        <td>$12.50</td>
    </tr>
    <tr>
        <td>14:11</td>
        <td>Expense</td>
        <td>$9.70</td>
    </tr>
    </tbody>
</table>

<script type="text/javascript">
    var trSelection = d3.selectAll("tr"); // <-- A
    var headerElement = trSelection.nodes()[0]; // <-- B
    d3.select(headerElement).attr("class", "table-header"); // <-
    - C
    var rows = trSelection.nodes();
    d3.select(rows[1]).attr("class", "table-row-odd"); // <-- D
    d3.select(rows[2]).attr("class", "table-row-even"); // <-- E
    d3.select(rows[3]).attr("class", "table-row-odd"); // <-- F
</script>

本例生成的视觉效果如图2-5所示。

图2-5 原始选集的处理

在本例中,我们遍历了一个页面上的HTML表格,并为之上色。事实上,这并非在D3下为表格的奇偶行上色的最好示例,但在这里,我们意在展示如何获取原始选集数组。

 

一个为表格奇偶行上色的更好方式是使用each函数,然后根据不同的索引参数进行处理。

在第A行中,我们选取了所有的行并将选集结果存储在变量中。D3选集提供了一个非常便利的函数,即node(),它会将选择的元素节点以数组的形式返回。因此,可以使用d3.selectAll("tr").nodes()[0]和d3.selectAll("tr").nodes()[1]来分别获得第一和第二个选中元素。在第B行中,表格的header元素可以通过trSelection.nodes[][0]来获取,得到DOM元素对象。在前面章节中我们提到过,任何DOM元素都可以直接通过d3.select来选取,如第C行所示。在第D、E、F行中,我们展示了如何对选集中的每个元素进行直接索引和访问。在某些情况下,特别是D3与其他JavaScript库搭配使用时,原始选集访问方式特别方便,因为其他库无法使用D3选集而只能使用原始的DOM元素。

 

这种方法通常在测试环境中是非常有用的,因为这种情况下知道每个元素的绝对下标,可以方便快捷地引用它们。关于这方面的话题,我们将在相关章节中详细介绍。

在本章中,我们介绍了使用D3的选集API来选择和操作HTML元素的各种方法。在下一章中,我们将探讨如何将数据与选集绑定到一起,以动态地驱动所选元素的视觉外观,这是数据可视化的基本步骤。


相关图书

Python数据科学实战
Python数据科学实战
数据分析实战:方法、工具与可视化
数据分析实战:方法、工具与可视化
Power BI数据挖掘与可视化分析
Power BI数据挖掘与可视化分析
从0到1——Python数据可视化
从0到1——Python数据可视化
善用图表——一看就懂的商业数据表达术
善用图表——一看就懂的商业数据表达术
从Power BI 到 Power Platform:低代码应用开发实战
从Power BI 到 Power Platform:低代码应用开发实战

相关文章

相关课程