整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

0基础学爬虫爬虫基础之网页基本结构

0基础学爬虫爬虫基础之网页基本结构

哥爬虫

大数据时代,各行各业对数据采集的需求日益增多,网络爬虫的运用也更为广泛,越来越多的人开始学习网络爬虫这项技术,K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章,为实现从易到难全方位覆盖,特设【0基础学爬虫】专栏,帮助小白快速入门爬虫,本期为网页基本结构介绍。

网页概述

网页是互联网应用的一种形态,是组成网站的基本元素。它是一个包含HTML标签的纯文本文件,可以存放在世界上任意一台计算机中。网页可以被看作为承载各种网站应用和信息的容器,网站的可视化信息都通过网页来进行展示,为网站用户提供一个友好的界面。

表面上,网页的组成可以分为文字、图片、音频、视频、超链接等元素构成,这些元素是用户能够直接看到的。但在本质上,网页的组成分为三部分:

  • HTML

HTML的全称为超文本标记语言,是一种标记语言,它是标准通用标记语言下的一个应用,也是一种规范,一种标准,它通过标记符号来标记要显示的网页中的各个部分。HTML文本是由HTML命令组成的描述性文本,HTML命令可以说明文字、图片、音频、视频、超链接等,用户在网页上看到的各种元素都是通过HTML文本来实现的。

  • CSS

网页的基本元素是通过HTML来实现的,但是HTML只能实现最基本的网页样式。随着HTML的发展,为了满足网页开发者的需求,CSS便孕育而生。

CSS全称为层叠样式表。它为HTML语言提供了一种样式描述,定义了元素的显示方式。提供了丰富的样式定义以及设置文本和背景属性的能力。CSS可以将所有的样式声明统一存放,进行统一管理。在CSS中,一个文件的样式可以从其他的样式表中继承。读者在有些地方可以使用他自己更喜欢的样式,在其他地方则继承或“层叠”作者的样式。这种层叠的方式使作者和读者都可以灵活地加入自己的设计,混合每个人的爱好。

  • JavaScript

JavaScript(JS)是一种面向对象的解释型脚本语言,它具有简单、动态、跨平台的特点。它被广泛应用与Web开发中,帮助开发者构建可拓展的交互式Web应用。JavaScript由三部分组成:

  • ECMAScript,描述了JS语言的基本语法与基本对象。
  • 文档对象模型(DOM),提供了处理网页结构内容的方法与接口。
  • 浏览器对象模型(BOM),提供了独立于内容而与浏览器窗口进行交互的方法与接口。

基本结构

网页的基本结构大致可以分为四部分:Doctype声明、html元素、head元素和body元素。

  • Doctype: 用来声明文档类型。它在HTML中的作用就是告诉浏览器以何种方式渲染页面。
  • html: html元素是网页的根元素,网页中的内容都会包含在html标签中。
  • head: head是所有头部元素的容器。被用来引用脚本文件、指示样式表存于何处。
  • body: body是网页的主体元素,用户在网页上浏览到的信息主要都存在于body之中,它包含网页文档的所有内容,如段落,列表,链接,图像,表格等。

元素、标签与属性

元素(Element)是网页的一部分,是构成网页的基本单位,实际上一个网页就是由多个元素构成的的文本文件。 标签(Tag)的作用就是用来定义元素。大多数的标签都是成对使用的,它存在一个开始标签与一个结尾标签,开始与结尾标签中间包含该元素的文本信息。

<div>这是一个div标签</div>
<p>这是一个p标签</p>

也有少部分的标签不成对。

<input>
<img>
<hr>
...

属性(attribute)主要是用来为标签添加额外的信息,属性的定义一般在开始标签中,以键值对的形式出现(name="value" ),属性的值应始终包括在引号内,属性和属性值对大小写不敏感,但是推荐使用小写的属性与属性值。一个标签可以拥有多个属性,也可以没有属性,开发者没有为标签定义属性的话则会使用默认属性。

<a href="https://www.kuaidaili.com/">这是一个a标签,href是我的属性。</a>

属性在HTML中被分为两种:通用属性和专用属性。 通用属性适用于大部分或所有标签之中,如:

  • class:规定元素的类名
  • id:规定元素的唯一id
  • style:规定元素的行内样式
  • title:规定元素的额外信息

专用属性适用于小部分标签或特定标签,如:

  • href:主要用于a标签与link标签中,规定元素的超链接地址
  • alt:主要用于img标签与area标签中,规定在图像无法显示时的替代文本

文档对象模型DOM

DOM全称即文档对象模型,是W3C制定的标准接口规范,是一种处理HTML和XML文件的标准API。DOM将HTML文本作为一个树形结构,DOM树的每个结点都表示了一个HTML标签或HTML标签内的文本项,它将网页与脚本或编程语言连接起来。

通过这个DOM树,开发者可以通过JavaScript来创建动态HTML,开发者借助JavaScript可以实现:

  • 动态改变页面中的所有HTML元素
  • 改变页面中的所有HTML属性
  • 改变页面中的所有CSS样式
  • 删除已有的HTML元素和属性
  • 添加新的HTML元素和属性
  • 对页面中所有已有的HTML事件作出反应
  • 在页面中创建新的HTML事件

DOM提供了一系列API来实现这些操作。

  • document.createElement:创建元素节点。 document.write:向文档写入内容。 element.innerHTML:向标签元素中添加内容。
  • element.removeChild:从DOM中删除一个子节点并返回删除的节点。 element.remove:把元素从它所属的DOM树中删除。 element.removeAttribute:从指定的元素中删除一个属性。
  • element.appendChild:将一个节点插入到指定父节点列表的末尾处。 parentNode.replaceChild:用一个节点替换当前节点中的一个子节点并返回被替换的节点。 parentNode.insertBefore:将一个节点插入到当前节点中一个子节点之前。
  • document.getElementById:返回一个元素Id与指定Id相匹配的元素。 document.getElementsByClassName:返回一个包含所有指定类名的元素的类数组对象。 document.querySelector:返回文档中与指定选择器或选择器组匹配的第一个Element对象。
  • 事件处理 EventTarget.addEventListener:将指定的监听器注册到EventTarget上,当事件被触发时,指定的回调函数就会被执行。 document.createEvent:创建一个指定类型的事件。 EventTarget.removeEventListener:移除事件监听器。

CSS选择器

css选择器是用来对HTML页面中的元素进行控制,通过对CSS选择器的了解,可以加深对网页结构与节点的理解。常用的CSS选择器主要分为:

1、元素选择器: 通过标签名{}的格式来选中对应标签,如:p{}

2、类选择器: 通过.类名{}的格式来选中对应类名的标签,如:.page{},page为元素的类名。

3、id选择器: 通过#id值{}的格式来选中对应id值的标签,如:#key{},key为元素的id值。

4、群组选择器: 通过选择器1,选择器2,选择器3...{}的格式来选中对应选择器的标签,如:div,.page{},即选择div标签下类名为pagae的标签。

5、子元素选择器: 通过父元素 > 子元素{}的格式来选中对应父元素中对应子元素的标签,如:div > p{},即选择div标签下的p标签,子元素选择器只能选择直接后代,不能跨节点选取。

6、后代选择器: 通过父元素 子元素{}的格式来选中对应父元素中对应子元素的标签,如:div p{},即选择div标签下的p标签,后代选择器可以跨节点选取。

谈一个网页打开的全过程(涉及DNS、CDN、Nginx负载均衡等)

1、概要

从用户在浏览器输入域名开始,到web页面加载完毕,这是一个说复杂不复杂,说简单不简单的过程,下文暂且把这个过程称作网页加载过程。下面我将依靠自己的经验,总结一下整个过程。如有错漏,欢迎指正。

阅读本文需要读者已有一定的计算机知识,了解TCP、DNS等。

2、分析

众所周知,打开一个网页的过程中,浏览器会因页面上的css/js/image等静态资源会多次发起连接请求,所以我们暂且把这个网页加载过程分成两部分:

  1. html(jsp/php/aspx) 页面加载(假设存在简单的Nginx负载均衡)
  2. css/js/image等 网页静态资源加载(假设使用CDN)

2.1 页面加载

先上一张图,直观明了地让大家了解下基本流程,然后我们再逐一分析。

2.1.1 DNS解析

什么是DNS解析?当用户输入一个网址并按下回车键的时候,浏览器得到了一个域名。而在实际通信过程中,我们需要的是一个IP地址。因此我们需要先把域名转换成相应的IP地址,这个过程称作DNS解析。

1) 浏览器首先搜索浏览器自身缓存的DNS记录。

或许很多人不知道,浏览器自身也带有一层DNS缓存。Chrome 缓存1000条DNS解析结果,缓存时间大概在一分钟左右。

(Chrome浏览器通过输入:chrome://net-internals/#dns 打开DNS缓存页面)

2) 如果浏览器缓存中没有找到需要的记录或记录已经过期,则搜索hosts文件和操作系统缓存。

在Windows操作系统中,可以通过 ipconfig /displaydns 命令查看本机当前的缓存。

通过hosts文件,你可以手动指定一个域名和其对应的IP解析结果,并且该结果一旦被使用,同样会被缓存到操作系统缓存中。

Windows系统的hosts文件在%systemroot%\system32\drivers\etc下,linux系统的hosts文件在/etc/hosts下。

3) 如果在hosts文件和操作系统缓存中没有找到需要的记录或记录已经过期,则向域名解析服务器发送解析请求。

其实第一台被访问的域名解析服务器就是我们平时在设置中填写的DNS服务器一项,当操作系统缓存中也没有命中的时候,系统会向DNS服务器正式发出解析请求。这里是真正意义上开始解析一个未知的域名。

一般一台域名解析服务器会被地理位置临近的大量用户使用(特别是ISP的DNS),一般常见的网站域名解析都能在这里命中。

4) 如果域名解析服务器也没有该域名的记录,则开始递归+迭代解析。

这里我们举个例子,如果我们要解析的是mail.google.com。

首先我们的域名解析服务器会向根域服务器(全球只有13台)发出请求。显然,仅凭13台服务器不可能把全球所有IP都记录下来。所以根域服务器记录的是com域服务器的IP、cn域服务器的IP、org域服务器的IP……。如果我们要查找.com结尾的域名,那么我们可以到com域服务器去进一步解析。所以其实这部分的域名解析过程是一个树形的搜索过程。

根域服务器告诉我们com域服务器的IP。

接着我们的域名解析服务器会向com域服务器发出请求。根域服务器并没有mail.google.com的IP,但是却有google.com域服务器的IP。

接着我们的域名解析服务器会向google.com域服务器发出请求。...

如此重复,直到获得mail.google.com的IP地址。

为什么是递归:问题由一开始的本机要解析mail.google.com变成域名解析服务器要解析mail.google.com,这是递归。

为什么是迭代:问题由向根域服务器发出请求变成向com域服务器发出请求再变成向google.com域发出请求,这是迭代。

5) 获取域名对应的IP后,一步步向上返回,直到返回给浏览器。

2.1.2 发起TCP请求

浏览器会选择一个大于1024的本机端口向目标IP地址的80端口发起TCP连接请求。经过标准的TCP握手流程,建立TCP连接。

关于TCP协议的细节,这里就不再阐述。这里只是简单地用一张图说明一下TCP的握手过程。如果不了解TCP,可以选择跳过此段,不影响本文其他部分的浏览。

2.1.3 发起HTTP请求

其本质是在建立起的TCP连接中,按照HTTP协议标准发送一个索要网页的请求。

2.1.4 负载均衡

什么是负载均衡?当一台服务器无法支持大量的用户访问时,将用户分摊到两个或多个服务器上的方法叫负载均衡。

什么是Nginx?Nginx是一款面向性能设计的HTTP服务器,相较于Apache、lighttpd具有占有内存少,稳定性高等优势。

负载均衡的方法很多,Nginx负载均衡、LVS-NAT、LVS-DR等。这里,我们以简单的Nginx负载均衡为例。关于负载均衡的多种方法详情大家可以Google一下。

Nginx有4种类型的模块:core、handlers、filters、load-balancers。

我们这里讨论其中的2种,分别是负责负载均衡的模块load-balancers和负责执行一系列过滤操作的filters模块。

1) 一般,如果我们的平台配备了负载均衡的话,前一步DNS解析获得的IP地址应该是我们Nginx负载均衡服务器的IP地址。所以,我们的浏览器将我们的网页请求发送到了Nginx负载均衡服务器上。

2) Nginx根据我们设定的分配算法和规则,选择一台后端的真实Web服务器,与之建立TCP连接、并转发我们浏览器发出去的网页请求。

Nginx默认支持 RR轮转法 和 ip_hash法 这2种分配算法。

前者会从头到尾一个个轮询所有Web服务器,而后者则对源IP使用hash函数确定应该转发到哪个Web服务器上,也能保证同一个IP的请求能发送到同一个Web服务器上实现会话粘连。

也有其他扩展分配算法,如:

fair:这种算法会选择相应时间最短的Web服务器

url_hash:这种算法会使得相同的url发送到同一个Web服务器

3) Web服务器收到请求,产生响应,并将网页发送给Nginx负载均衡服务器。

4) Nginx负载均衡服务器将网页传递给filters链处理,之后发回给我们的浏览器。

而Filter的功能可以理解成先把前一步生成的结果处理一遍,再返回给浏览器。比如可以将前面没有压缩的网页用gzip压缩后再返回给浏览器。

2.1.5 浏览器渲染

1) 浏览器根据页面内容,生成DOM Tree。根据CSS内容,生成CSS Rule Tree(规则树)。调用JS执行引擎执行JS代码。

2) 根据DOM Tree和CSS Rule Tree生成Render Tree(呈现树)

3) 根据Render Tree渲染网页

但是在浏览器解析页面内容的时候,会发现页面引用了其他未加载的image、css文件、js文件等静态内容,因此开始了第二部分。

2.2 网页静态资源加载

以阿里巴巴的淘宝网首页的logo为例,其url地址为 img.alicdn.com/tps/i2/TB1bNE7LFXXXXaOXFXXwFSA1XXX-292-116.png_145x145.jpg

我们清楚地看到了url中有cdn字样。

什么是CDN?如果我在广州访问杭州的淘宝网,跨省的通信必然造成延迟。如果淘宝网能在广东建立一个服务器,静态资源我可以直接从就近的广东服务器获取,必然能提高整个网站的打开速度,这就是CDN。CDN叫内容分发网络,是依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度。

接下来的流程就是浏览器根据url加载该url下的图片内容。本质上是浏览器重新开始第一部分的流程,所以这里不再重复阐述。区别只是负责均衡服务器后端的服务器不再是应用服务器,而是提供静态资源的服务器。

文章乃参考、转载其他博客所得,仅供自己学习作笔记使用!!!


章涵盖

  • 从分层数据构造根节点。
  • 使用分层布局生成器。
  • 绘制一个圆形包。
  • 绘制树形图。

在本书的前几章中,我们使用数据可视化来编码数值数据。例如,第3章条形图中条形的长度表示相关调查回复的数量。同样,第4章折线图中数据点的位置描绘了温度。在本章中,我们将讨论分层可视化,它对父子关系进行编码,并且可以揭示迄今为止我们使用的更简单的可视化所没有注意到的模式。

分层可视化通过外壳连接邻接来传达父子关系。图 11.1 显示了使用外壳的分层可视化的两个示例:圆形包和树状图。顾名思义,圆包是一组圆圈。有一个根父级,最外圈,以及称为节点的后续子级。节点的所有子节点都“打包”到该节点中,圆圈的大小与它们包含的节点数成正比。叶节点的大小(最低级别的子节点)可以表示任意属性。树状图的工作方式类似,但使用嵌套矩形而不是圆形。树状图比圆形包更节省空间,我们经常在与财务相关的可视化中遇到它们。

可视化父子关系的一种熟悉且直观的方法是通过连接,例如在树形图中。树形图可以是线性的,如家谱树,也可以是径向的,如图 11.1 所示。线性树形图更易于阅读,但会占用大量空间,而径向树更紧凑,但需要更多的努力来破译。

最后,我们可以通过冰柱图(也称为分区层图)的邻接来可视化层次结构模式。我们经常在IT(信息技术)中遇到这样的图表。

图 11.1 在分层可视化中,父子关系可以通过外壳(如在圆形包或树状图中)或通过连接(如在树形图中)或邻接(如在冰柱图中)进行通信。


图 11.1 中显示的图表可能看起来多种多样,但使用 D3 构建它们意味着涉及布局生成器函数的类似过程。在第 5 章中,我们了解了 D3 的布局生成器函数如何将信息添加到现有数据集,以及我们可以使用此信息将所需的形状附加到 SVG 容器中。创建分层可视化也不例外。

在本章中,我们将构建两个分层可视化:圆形包和线性树形图。我们将基于世界上 100 种使用最多的语言的数据集进行可视化。您可以看到我们将在 https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/ 构建的图表。

我们数据集中的每种语言都属于一个语言家族或一组从共同祖先发展而来的相关语言。这些家族可以细分为称为分支的较小组。让我们以五种最常用的语言为例。在表 11.1 中,我们看到了如何将每种语言的信息存储在电子表格中。左列包含语言:英语、中文普通话、印地语、西班牙语和法语。以下列包括相关的语系:印欧语系和汉藏语系,以及语言分支:日耳曼语系、汉尼特语系、印度-雅利安语系和罗曼语系。我们用每种语言的使用者总数和母语人士的数量来完成表格。

表11.1 世界上使用最多的5种语言

语言

家庭

分支

演讲者总数

母语人士

英语

印欧语系

日耳曼

1,132m

379m

普通话

汉藏语

西尼特

1,117m

918m

印地语

印欧语系

印度-雅利安语

615m

341m

西班牙语

印欧语系

浪漫

534m

460m

法语

印欧语系

浪漫

280m

77m

分层可视化具有单个根节点,该节点分为多个以结尾的分支。在表 11.1 的示例数据集中,节点可以称为“语言”,如图 11.2 所示。词根分为两个语系:印欧语系和汉藏语系,也分为分支:日耳曼语系、印度-雅利安语系、罗曼语系和汉尼语系。最后,叶子出现在图形的右侧:英语、印地语、西班牙语、法语和普通话。每种语言、分支、族和根称为一个节点

图 11.2 分层可视化具有一个分离为一个或多个分支的单个根。它以叶节点结尾,在此示例中是语言。


在本书的前半部分,我们主要使用类似遗产的项目结构。主要目标是进行简单的设置,并专注于D3。但是,如果您发布 D3 项目,则很有可能使用 JavaScript 模块导入。在本章中,我们将对项目结构进行现代化改造,以允许单独导入 D3 模块。它将使我们的项目文件更小,因此加载速度更快,并且将是查看哪些 D3 模块包含哪种方法的绝佳机会。这些知识将使您将来更容易搜索 D3 文档。

要将我们的 JavaScript 文件和 npm 模块组合成一个浏览器可读的模块,我们需要一个捆绑器。您可能已经熟悉 Webpack 或 RollUp。由于此类工具可能需要相当多的配置,因此我们将转向Parcel(https://parceljs.org/),这是一个非常易于使用且需要接近零的配置的捆绑器。

如果您的计算机上尚未安装 Parcel,则可以使用以下命令对其进行全局安装,其中 -g 代表全局。在终端窗口中运行此命令。

npm install -g parcel

我们建议使用此类全局安装,因为它将使 Parcel 可用于您的所有项目。请注意,根据计算机的配置,您可能需要在命令的开头添加 Mac 和 Linux 上的术语 sudo 或 Windows 上的 runas。

在您的计算机上安装 Parcel 后,在代码编辑器 (https://github.com/d3js-in-action-third-edition/code-files/tree/main/chapter_11/11.1-Formatting_hierarchical_data/start) 中打开本章代码文件的起始文件夹。如果您使用的是 VS Code,请打开集成终端并运行命令 npm install 以安装项目依赖项。在此阶段,我们唯一的依赖项是允许我们稍后加载CSV数据文件。

若要启动项目,请运行命令包,后跟根文件的路径:

parcel src/index.html

在浏览器中打开 http://localhost:1234/ 以查看您的项目。每次保存文件时,浏览器中显示的项目都会自动更新。完成工作会话后,您可以通过输入终端 ctrl + C 来停止包裹。

在文件索引中.html ,我们已经加载了带有脚本标签的文件 main.js。因为我们将使用模块,所以我们将脚本标记的 type 属性设置为 module 。JavaScript 模块的好处是,我们不需要将额外的脚本加载到 index 中.html ;一切都将从主.js.请注意,我们也不需要使用脚本标记加载 D3 库。我们将从下一节开始安装和导入所需的 D3 模块。

11.1 格式化分层数据

为了创建分层可视化,D3 希望我们以特定方式格式化数据。我们有两个主要选项:使用 CSV 文件或使用分层 JSON。

11.1.1 使用 CSV 文件

我们的大多数数据都以表格形式出现,通常以电子表格的形式出现。此类文件必须通过列指示父子关系。在表 11.2 中,我们将五种最常用的语言的示例数据集重新组织为名为“child”和“parent”的列。稍后我们将使用这些列名称,让 D3 知道如何建立父子关系。在第一行中,子列中有根节点“语言”。由于这是根节点,因此它没有父节点。然后,在下面的行中,我们列出了根的直系子女:印欧语系和汉藏语系。他们都有“语言”作为父母。我们遵循语言分支(日耳曼语、汉尼特语、印度-雅利安语和罗曼语),并声明哪个语系是它们的父语言。最后,每种语言(英语、中文普通话、印地语、西班牙语和法语)都有一行,并设置它们的父语言,即相关语言分支。我们还为每种语言设置了“total_speakers”和“native_speakers”列,因为我们可以在可视化中使用此信息,但这些信息对于分层布局不是必需的。

表 11.2 显示了在使用 D3 构建分层可视化之前我们如何构建电子表格。然后,我们将其导出为CSV文件并将其添加到我们的项目中。请注意,您不必为本章的练习制作自己的电子表格。您可以在 /data 文件夹中找到 100 种最常用的语言(名为 flat_data.csv)格式正确的 CSV 文件。

表 11.2 在 CSV 文件中,我们通过列传达父子关系

孩子

父母

使用

母语

语言




印欧语系

语言



汉藏语

语言



日耳曼

印欧语系



西尼特

汉藏语



印度-雅利安语

印欧语系



浪漫

印欧语系



浪漫

印欧语系



英语

日耳曼

1,132m

379m

普通话

西尼特

1,117m

918m

印地语

印度-雅利安语

615m

341m

西班牙语

浪漫

534m

460m

法语

浪漫

280m

77m

让我们flat_data.csv加载到我们的项目中!首先,在 /js 文件夹中创建一个新的 JavaScript 文件。将其命名为 load-data.js ,因为这是我们加载数据集的地方。在清单 11.1 中,我们创建了一个名为 loadCSVData() 的函数。我们向函数添加导出声明,使其可供项目中的其他 JavaScript 模块访问。


要将 CSV 文件加载到我们的项目中,我们需要采用与使用 d3.csv() 方法不同的路线。宗地需要适当的转换器才能加载 CSV 文件。我们已经通过安装允许 Parcel 解析 CSV 文件的模块为您完成了项目中的所有配置(有关更多详细信息,请参阅文件 .parcelrc 和 .parcel-transformer-csv.json)。我们现在要做的就是使用 JavaScript require() 函数加载 CSV 文件并将其保存到常量 csvData 中。如果将 csvData 登录到控制台,您将看到它由一个对象数组组成,每个对象对应于 CSV 文件中的一行。我们遍历 csvData 将说话者的数量格式化为数字并返回 csvData .

示例 11.1 将 CSV 文件加载到项目中(加载数据.js)

export const loadCSVData=()=> {
 
  const csvData=require("../data/flat_data.csv");  #A
 
  csvData.forEach(d=> {                     #B
    d.total_speakers=+d.total_speakers;    #B
    d.native_speakers=+d.native_speakers;  #B
  });                                        #B
 
  return csvData;
 
};

在 main.js 中,我们使用导入语句来访问函数 loadCSVData(),如清单 11.2 所示。然后我们将 loadCSVData() 返回的数组保存到一个名为 flatData 的常量中。

清单 11.2 将平面数据导入主数据.js (main.js)

import { loadCSVData } from "./load-data.js"; #A
 
const flatData=loadCSVData();  #B

下一步是将平面 CSV 数据转换为分层格式,或包含其子节点的根节点。d3-hierarchy 模块 (https://github.com/d3/d3-hierarchy) 包含一个名为 d3.stratify() 的方法,它就是这样做的。它还包括构建分层可视化所需的所有其他方法。

为了最大限度地提高项目性能,我们不会安装整个 D3 库,而只会安装我们需要的模块。让我们从 d3 层次结构开始。在 VS Code 中,打开一个新的终端窗口并运行以下命令:

npm install d3-hierarchy

然后,创建一个名为 hierarchy 的新 JavaScript 文件.js 。在文件顶部,从 d3-hierarchy 导入 stratify() 方法,如清单 11.3 所示。然后,创建一个名为 CSVToHierarchy() 的函数,该函数将 CSV 数据作为参数。请注意,我们通过导出声明提供此功能。

在 CSVToHierarchy() 中,我们通过调用方法 stratify() 来声明一个层次结构生成器。在我们之前的设置中,我们会用 d3.stratify() 调用此方法。因为我们只安装了 d3-hierarchy 模块,所以我们不再需要在 d3 对象上调用方法并将 stratify() 视为一个独立的函数。

要将我们的CSV数据转换为分层结构,函数stratify()需要知道如何建立父子关系。使用 id() 访问器函数,我们指示可以在哪个键下找到子项,在本例中为 子项(子项存储在原始 CSV 文件的“子项”列中)。使用 parentId() 访问器函数,我们指示可以在哪个键下找到父级,在我们的例子中是父级(父级存储在原始 CSV 文件的“父级”列中)。

我们将数据传递给层次结构生成器,并将其保存在一个名为 root 的常量中,这是我们的分层数据结构。这种嵌套数据结构带有一些方法,如 descendants(),它返回树中所有节点的数组(“语言”、“印欧语”、“日耳曼语”、“英语”等),以及 leaves() 返回所有没有子节点的数组(“英语”、“普通话”、“印地语”等)。我们将后代节点和叶节点保存到常量中,并使用根数据结构返回它们。

11.3 将CSV数据转换为层次结构(层次结构.js)

import { stratify } from "d3-hierarchy";  #A
 
export const CSVToHierarchy=(data)=> {
 
  const hierarchyGenerator=stratify()   #B
    .id(d=> d.child)                     #B
    .parentId(d=> d.parent);             #B
  const root=hierarchyGenerator(data);  #C
 
  const descendants=root.descendants(); #D
  const leaves=root.leaves();           #D
 
  return [root, descendants, leaves];
 
};

在 main.js 中,我们导入函数 CSVToHierarchy() 并调用它来获取根、后代和叶。我们将在以下部分中使用此层次结构数据结构来生成可视化效果。

示例 11.4 将层次结构数据结构导入 main.js (main.js)

import { loadCSVData } from "./load-data.js";
import { CSVToHierarchy } from "./hierarchy.js";
 
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);

11.1.2 使用分层 JSON 文件

我们的数据集也可以存储为分层 JSON 文件。JSON 本质上支持分层数据结构,并使其易于理解。以下 JSON 对象演示如何为示例数据集构建数据。在文件的根目录中,我们有一个用大括号 ( {} ) 括起来的对象。根的“name”属性是“语言”,其“子”属性是一个对象数组。根的每个直接子级都是一个语言家族,其中包含语言分支的“子”数组,其中还包括带有语言叶的“子”数组。请注意,每个子项都存储在一个对象中。我们可以在叶对象中添加与语言相关的其他数据,例如说话者和母语人士的总数,但这是可选的。

{
  "name": "Languages",
  "children": [
    {
      "name": "Indo-European",
      "children": [
        {
          "name": "Germanic",
          "children": [
            {
              "name": "English"
            }
          ]
        },
        {
          "name": "Indo-Aryan",
          "children": [
            {
              "name": "Hindi"
            }
          ]
        },
        {
          "name": "Romance",
          "children": [
            {
                  "name": "Spanish"
            },
            {
                  "name": "French"
            }
          ]
        },
      ]
    },
    {
      "name": "Sino-Tibetan",
      "children": [
        {
          "name": "Sinitic",
          "children": [
            {
              "name": "Mandarin Chinese"
            }
          ]
        }
      ]
    }
  ]
}

分层 JSON 文件已在数据文件夹 ( hierarchical-data.json ) 中可用。我们将以类似的方式处理 CSV 文件以将其加载到我们的项目中。在清单 11.5 中,我们回到 load-data.js 并创建一个名为 loadJSONData() 的函数。此函数使用 JavaScript require() 方法来获取数据集并将其存储在名为 jsonData 的常量中。常量 jsonData 由函数返回。

示例 11.5 将 JSON 数据加载到项目中 (load-data.js)

export const loadJSONData=()=> {
 
  const jsonData=require("../data/hierarchical-data.json");
 
  return jsonData;
 
};

回到main.js,我们导入loadJSONData(),调用它并将它返回的对象存储到一个名为jsonData的常量中。

清单 11.6 将 JSON 数据导入 main.js (main.js)

import { loadCSVData, loadJSONData } from "./load-data.js";
import { CSVToHierarchy } from "./hierarchy.js";
 
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
 
const jsonData=loadJSONData();

为了从 JSON 文件生成分层数据结构,我们使用方法 d3.hierarchy() 。在示例 11.7 中,我们从 d3-hierarchy 导入层次结构函数。然后我们创建一个名为 JSONToHierarchy() 的函数,它将 JSON 数据作为参数。

我们调用 hierarchy() 函数并将数据作为参数传递。我们将它返回的嵌套数据结构存储在名为 root 的常量中。与之前由 stratify() 函数返回的数据结构一样,root 有一个方法后代 (),它返回树中所有节点的数组(“语言”、“印欧语”、“日耳曼语”、“英语”等),还有一个方法 leaves() 返回所有没有子节点的数组(“英语”、“普通话”、“印地语”等)。我们将后代节点和叶节点保存到常量中,并使用根数据结构返回它们。

11.7 将 JSON 数据转换为层次结构(层次结构.js)

import { stratify, hierarchy } from "d3-hierarchy";
 
...
 
export const JSONToHierarchy=(data)=> {
 
  const root=hierarchy(data);
 
  const descendants=root.descendants();
  const leaves=root.leaves();
 
  return [root, descendants, leaves];
 
};

最后,在main.js中,我们导入根,后代和叶子数据结构。为了将它们与从 CSV 数据导入的后缀区分开来,我们添加了_j后缀。

示例 11.8 将层次结构数据结构导入 main.js (main.js)

import { loadCSVData, loadJSONData } from "./load-data.js";
import { CSVToHierarchy, JSONToHierarchy } from "./hierarchy.js";
 
const flatData=loadCSVData();
const [root, descendants, leaves]=CSVToHierarchy(flatData);
 
const jsonData=loadJSONData();
const [root_j, descendants_j, leaves_j]=JSONToHierarchy(jsonData);

注意

在现实生活中的项目中,我们不需要同时加载CSV和JSON数据;这将是一个或另一个。我们这样做只是出于教学目的。

有两种主要方法可以将分层数据加载到 D3 项目中:从 CSV 文件或分层 JSON 文件。如图 11.3 所示,如果我们使用 CSV 文件中的数据,我们会将其传递给 d3.stratify() 以生成层次结构数据结构。如果我们使用 JSON 文件,我们将使用 d3.hierarchy() 方法代替。这两种方法都返回相同的嵌套数据结构,通常称为 root 。此根有一个返回层次结构中所有节点的方法 descendants() 和一个返回没有子节点的方法 leaves()。

图 11.3 我们以 CSV 或 JSON 文件的形式获取分层数据。如果我们使用 CSV 文件,我们将数据传递给 d3.stratify() 以生成层次结构。如果我们使用 JSON 文件,我们将使用 d3.hierarchy() 方法代替。这些方法生成的嵌套数据结构是相同的,通常存储在名为 root 的常量中。它有一个返回层次结构中所有节点的方法 descendants(),以及一个返回所有没有子节点的方法 leaves()。


现在我们的分层数据已经准备就绪,我们可以进入有趣的部分并构建可视化!这就是我们将在以下部分中执行的操作。

11.2 构建圆形装箱图

在圆形包中,我们用圆圈表示每个节点,子节点嵌套在其父节点中。当我们想要一目了然地理解整个分层组织时,这种可视化很有帮助。它易于理解,外观令人愉悦。

在本节中,我们将使用图 100.11 所示的圆形包可视化我们的 4 种最常用的语言数据集。在此可视化中,最外层的圆圈是根节点,我们将其命名为“语言”。颜色较深的圆圈是语系,颜色较浅的圆圈是语言分支。白色圆圈表示语言,其大小表示说话者的数量。

图 11.4 我们将在本节中构建的 100 种最常用的语言的 circle pack 可视化。


11.2.1 生成包布局

要使用 D3 创建圆形包装可视化,我们需要使用布局生成器。与第5章中讨论的类似,此类生成器将现有数据集附加构建图表所需的信息,例如每个圆和节点的位置和半径,如图11.5所示。

图 11.5 要使用 D3 创建圆形包可视化,我们从分层数据(通常称为 root)开始。为了计算布局,我们调用根 sum() 方法来计算圆圈的大小。然后,我们将根数据结构传递给 D3 的 pack() 布局生成器。此生成器将每个圆的位置和半径附加到数据中。最后,我们将圆圈附加到 SVG 容器,并使用附加的数据设置它们的位置和大小。



我们已经有了名为 root 的分层数据结构,并准备跳到图 11.5 中的第二步。首先,让我们创建一个名为 circle-pack 的新 JavaScript 文件.js并声明一个名为 drawCirclePack() 的函数。此函数采用根、后代,并将上一节中创建的数据结构保留为参数。

export const drawCirclePack=(root, descendants, leaves)=> {};

在main.js中,我们导入drawCirclePack()并调用它,将根,后代和叶作为参数传递。

import { drawCirclePack } from "./circle-pack.js";
 
drawCirclePack(root, descendants, leaves);

回到 circle-pack.js ,在函数 drawCirclePack() 中,我们将开始计算我们的布局。在示例 11.9 中,我们首先声明图表的维度。我们将宽度和高度都设置为 800px。然后,我们声明一个边距对象,其中上边距、右边距、下边距和左边距等于 1px。我们需要此边距才能看到可视化的最外层圆圈。最后,我们使用第 4 章中采用的策略计算图表的内部宽度和高度。

然后,我们调用 sum() 方法,该方法可用于 root。此方法负责计算可视化效果的聚合大小。我们还向 D3 指示应从中计算叶节点半径的键:total_speakers 。

为了初始化包布局生成器,我们调用 D3 方法 pack() ,我们从文件顶部的 d3-hierarchy 导入该方法。我们使用它的 size() 访问函数来设置圆形包的整体大小,并使用 padding() 函数将圆圈之间的空间设置为 3px。

示例 11.9 计算包布局(圆形包.js)

import { pack } from "d3-hierarchy";
 
export const drawCirclePack=(root, descendants, leaves)=> {
 
  const width=800;                                        #A
  const height=800;                                       #A
  const margin={ top: 1, right: 1, bottom: 1, left: 1 };  #A
  const innerWidth=width - margin.right - margin.left;    #A
  const innerHeight=height - margin.top - margin.bottom;  #A
 
  root.sum(d=> d.total_speakers);  #B
 
  const packLayoutGenerator=pack()  #C
    .size([innerWidth, innerHeight])  #C
    .padding(3);                      #C
  packLayoutGenerator(root);  #D
 
};

如果将后代数组记录到控制台中,您将看到包布局生成器为每个节点追加了以下信息:

  • id :节点的标签。
  • 深度:根节点为 0,语言家族为 1,语言分支为 2,语言分支为 3。
  • r :节点圆的半径。
  • x :节点圆心的水平位置。
  • y :节点圆心的垂直位置。
  • data :包含节点父节点名称、说话人总数和母语说话人数量的对象。

我们将在下一节中使用此信息来绘制圆形包。

11.2.2 绘制圆形包

我们现在准备绘制我们的圆形包!要选择元素并将其附加到 DOM,我们需要通过在终端中运行 npm install d3-select 来安装 d3 选择模块 (https://github.com/d3/d3-selection)。此模块包含负责操作 DOM、应用数据绑定模式和侦听事件的 D3 方法。在 circle-pack 的顶部.js ,我们从 d3-select 导入 select() 函数。

在 drawCirclePack() 中,我们将一个 SVG 容器附加到 div 中,其 id 为 “circle-pack”,该容器已存在于索引中.html 。我们按照第 4 章中解释的策略设置其 viewBox 属性并附加一个组以包含内部图表。

然后,我们为后代数组中的每个节点附加一个圆圈。我们使用包布局生成器附加到数据的 x 和 y 值来设置它们的 cx 和 cy 属性。我们对半径做同样的事情。现在,我们将圆圈的填充属性设置为“透明”,将其笔触设置为“黑色”。我们稍后会改变这一点。

在 11.10 绘制圆形包(圆形包.js)

import { pack } from "d3-hierarchy";
import { select } from "d3-selection";
 
export const drawCirclePack=(root, descendants, leaves)=> {
 
  ...
 
  const svg=select("#circle-pack")                                  #A
    .append("svg")                                                    #A
      .attr("viewBox", `0 0 ${width} ${height}`)                      #A
      .append("g")                                                    #A
      .attr("transform", `translate(${margin.left}, ${margin.top})`); #A
 
     svg                                #B
    .selectAll(".pack-circle")       #B
    .data(descendants)               #B
    .join("circle")                  #B
      .attr("class", "pack-circle")  #B
      .attr("cx", d=> d.x)          #C
      .attr("cy", d=> d.y)          #C
      .attr("r", d=> d.r)           #C
      .attr("fill", "none")
      .attr("stroke", "black");
 
};

完成此步骤后,您的圆形包应如图 11.6 所示。我们的可视化正在形成!

图 11.6 在这个阶段,我们的圆形包的形状已经准备好了。接下来,我们将添加颜色和标签。



我们希望圆圈包中的每个语言家族都有自己的颜色。如果打开文件帮助程序.js ,您将看到一个名为 languageFamilies 的数组。它包含语言系列及其相关颜色的列表,如以下代码片段所示。我们可以使用此数组来创建色阶并使用它来设置每个圆的填充属性。

export const languageFamilies=[
  { label: "Indo-European", color: "#4E86A5" },
  { label: "Sino-Tibetan", color: "#9E4E9E" },
  { label: "Afro-Asiatic", color: "#59C8DC" },
  { label: "Austronesian", color: "#3E527B" },
  { label: "Japanic", color: "#F99E23" },
  { label: "Niger-Congo", color: "#F36F5E" },
  { label: "Dravidian", color: "#C33D54" },
  { label: "Turkic", color: "#D57AB1" },
  { label: "Koreanic", color: "#33936F" },
  { label: "Kra-Dai", color: "#36311F" },
  { label: "Uralic", color: "#B59930" },
];

要使用 D3 缩放,我们需要安装 d3 缩放模块 (https://github.com/d3/d3-scale) npm 安装 d3-scale 。对于我们的色阶,我们将使用序数刻度,它采用离散数组作为域、语言系列,将离散数组作为范围,即关联的颜色。在示例 11.11 中,我们创建了一个名为 scales.js 的新文件。在文件的顶部,我们从 d3-scale 导入 scaleOrdinal,从 helper.js 导入我们的 languageFamilies 数组。然后,我们声明一个名为 colorScale 的序数刻度,传递一个语言家族标签数组作为域,传递一个关联颜色数组作为范围。我们使用 JavaScript map() 方法生成这些数组。

11.11 创建色阶(scales.js)

import { scaleOrdinal } from "d3-scale";
import { languageFamilies } from "./helper";
 
export const colorScale=scaleOrdinal()
  .domain(languageFamilies.map(d=> d.label))
  .range(languageFamilies.map(d=> d.color));

在第 11.2.1 节结束时,我们讨论了 D3 包布局生成器如何将多条信息附加到后代数据集(也称为节点),包括它们的深度。图 11.7 显示我们的圆形包的深度从 2 到 3 不等。表示“语言”根节点的最外层圆的深度为零。此圆圈具有灰色边框和透明填充。以下圆圈是深度为一的语言家族。它们的 fill 属性对应于我们刚刚声明的色阶返回的颜色。然后,语言分支的深度为 <>。他们继承了父母颜色的更苍白版本。最后,叶节点或语言的深度为 <>,颜色为白色。这种颜色渐变不遵循任何特定规则,但旨在使父子关系尽可能明确。

图 11.7 在我们的圆包上,最外层的圆圈是语言,深度为零。遵循深度为 <> 的语言家族、深度为 <> 的语言分支和深度为 <> 的语言。


回到 circle-pack.js ,我们将使用色阶设置圆圈的填充属性。在文件的顶部,我们导入之前以比例创建的色阶.js .为了生成语言分支的较浅颜色(深度为 2 的圆圈),我们将使用称为插值的 d3 方法,该方法在 d3-插值模块 (https://github.com/d3/d3-interpolate) 中可用。使用 npm 安装 d3 插值安装此模块,并将此方法导入到圆包的顶部.js .

在示例 11.12 中,我们回到设置圆圈填充属性的代码。我们使用 JavaScript switch() 语句来评估附加到每个节点的深度数的值。如果深度为 3,则节点是一个语言家族。我们将它的 id 传递给色标,色标返回关联的颜色。对于语言分支,我们仍然调用色阶,但在其父节点的值上(d.parent.id)。然后,我们将比例返回的颜色作为 d0 插值() 函数的第一个参数传递。第二个参数是“白色”,即我们想要插入初始值的颜色。我们还将值 5.50 传递给 interpolate() 函数,以指示我们想要一个介于原始颜色和白色之间的 <>% 的值。最后,我们为所有剩余节点返回默认填充属性“white”。

我们还更改了圆圈的笔触属性。如果深度为零,因此节点是最外层的圆,我们给它一个灰色的笔触。否则,不应用笔画。

11.12 根据深度和语言族对圆圈应用颜色(圆圈包.js)

...
import { colorScale } from "./scales";
import { interpolate } from "d3-interpolate";
 
export const drawCirclePack=(root, descendants, leaves)=> {
 
  ...
 
  svg
    .selectAll(".pack-circle")
    .data(descendants)
    .join("circle")
      .attr("class", "pack-circle")
      ...
      .attr("fill", d=> {
        switch (d.depth) {  #A
          case 1:                     #B
            return colorScale(d.id);  #B
          case 2:                                                       #C
            return interpolate(colorScale(d.parent.id), "white")(0.5);  #C
          default:           #D
            return "white";  #D
        };
      })
      .attr("stroke", d=> d.depth===0 ? "grey" : "none");  #E
 
};

完成后,您的彩色圆圈包应如图 11.8 所示。

图 11.8 应用颜色后的圆形包装。


11.2.3 添加标签

我们的圆圈包绝对看起来不错,但没有提供任何关于哪个圆圈代表哪种语言、分支或家族的线索。圆形包装的主要缺点之一是在保持其可读性的同时在其上贴标签并不容易。但是由于我们正在从事数字项目,因此我们可以通过鼠标交互向读者提供其他信息。

在本节中,我们将首先为较大的语言圈添加标签。然后,我们将构建一个交互式工具,当鼠标位于叶节点上时,该工具可提供其他信息。

使用 HTML div 应用标签

在我们的可视化中,我们处理的是名称相对较短的语言,如法语或德语,以及其他名称较长的语言,如“现代标准阿拉伯语”或“西旁遮普语”。要在相应的圆圈内显示这些较长的标签,我们需要让它们分成多行。但是,如果您还记得我们之前关于 SVG 文本元素的讨论,则可以在多行上显示它们,但需要大量工作。使用常规 HTML 文本在需要时自动换行,这要容易得多!猜猜看:我们可以在 SVG 元素中使用常规 HTML 元素,这正是我们在这里要做的。

SVG 元素 foreignObject 允许我们在 SVG 容器中包含常规 HTML 元素,例如 div 。然后这个div可以像任何其他div一样设置样式,并且它的文本将在需要时自动换行。

在 D3 中,我们附加 foreignObject 元素的方式与其他任何元素相同。然后,在这些外来对象元素中,我们附加我们需要的 div。您可以将这些视为SVG和HTML世界之间的网关。

出于可读性的目的,我们不会在每个语言圈上应用标签,而只会在较大的语言圈上应用标签。在示例 11.13 中,我们首先定义要应用标签的圆的最小半径,即 22px。然后,我们使用数据绑定模式为每个满足最小半径要求的叶节点附加一个 foreignObject 元素。外来对象元素有四个必需的属性:

  • 宽度:元素的宽度,对应于圆的直径(2 * d.r)。
  • 高度 :元素的高度。在这里,我们应用40px的高度,这将为三行文本留出空间。
  • x :左上角的水平位置。我们希望这个角与圆的左侧匹配 ( d.x - d.r )。
  • y :左上角的垂直位置,我们希望在圆心上方 20px,因为 foreignObject 元素的总高度为 40px ( d.y - 20 )。

然后,我们需要指定要附加到 foreignObject 中的元素的 XML 命名空间。这就是为什么我们附加一个 xhtml:div 而不仅仅是一个 div .我们给这个div一个类名“leaf-label”,并将其文本设置为节点的id。文件可视化.css 已包含在 foreignObject 元素内水平和垂直居中标签所需的样式。

11.13 使用外来对象元素应用标签(圆形包装.js)

export const drawCirclePack=(root, descendants, leaves)=> {
 
  ...
 
  const minRadius=22;
  svg
    .selectAll(".leaf-label-container")                  #A
    .data(leaves.filter(leave=> leave.r >=minRadius))  #A
    .join("foreignObject")                               #A
      .attr("class", "leaf-label-container")
      .attr("width", d=> 2 * d.r)  #B
      .attr("height", 40)           #B
      .attr("x", d=> d.x - d.r)    #B
      .attr("y", d=> d.y - 20)     #B
    .append("xhtml:div")            #C
      .attr("class", "leaf-label")  #C
      .text(d=> d.id);             #C
 
};

应用标签后,您的圆形包应如图 11.4 和托管项目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 中的包所示。现在,我们可以在可视化中找到主要语言。

使用工具提示提供其他信息

在第 7 章中,我们讨论了如何使用 D3 侦听鼠标事件,例如显示工具提示。在本节中,我们将构建类似的东西,但不是在可视化效果上显示工具提示,而是将其移动到一侧。只要您有超过几行信息要向用户显示,这是一个很好的选择。由于此类工具提示是使用 HTML 元素构建的,因此也更容易设置样式。

在文件索引中.html ,取消注释 id 为“信息容器”的 div。此 div 包含两个主要元素:

  • 默认情况下显示 id 为“指令”的 div,指示用户将鼠标移到圆圈上以显示其他信息。
  • ID 为“info”的 div,将显示有关悬停节点的语言、分支、系列和说话人数量的信息。此 div 还有一个“隐藏”类,它将其最大高度和不透明度属性设置为零,使其不可见。您可以在可视化中找到相关样式.css 。

在示例 11.14 中,我们又回到了 circle-pack.js 。在文件顶部,我们从 d3-select 导入 selectAll 函数。我们还需要安装 d3 格式模块 (https://github.com/d3/d3-format) 并导入其格式函数。

为了区分节点级别,在清单 11.14 中,我们将它们的深度值添加到它们的类名中。然后,我们使用 selectAll() 函数选择所有类名为 “pack-circle-depth-3” 的圆圈和所有 foreignObject 元素。我们使用 D3 on() 方法将 mouseenter 事件侦听器附加到叶节点及其标签。在此事件侦听器的回调函数中,我们使用附加到元素的数据来填充有关相应语言、分支、家族和说话人数量的工具提示信息。请注意,我们使用 format() 函数来显示具有三个有效数字和后缀的扬声器数量,例如“M”表示“百万”(“.3s”);

然后,我们通过添加和删除类名“hidden”来隐藏说明并显示工具提示。我们还在鼠标离开语言节点或其标签时应用事件侦听器。在其回调函数中,我们隐藏工具提示并显示说明。

示例 11.14 侦听叶节点上的鼠标事件并显示附加信息 (circle-pack.js)

import { select, selectAll } from "d3-selection";
import { format } from "d3-format";
 
export const drawCirclePack=(root, descendants, leaves)=> {
 
  ...
 
  svg
    .selectAll(".pack-circle")
    .data(descendants)
    .join("circle")
      .attr("class", d=> `pack-circle pack-circle-depth-${d.depth}`)  #A
      ...
 
  selectAll(".pack-circle-depth-3, foreignObject")  #B
    .on("mouseenter", (e, d)=> {  #C
 
      select("#info .info-language").text(d.id);                    #D
      select("#info .info-branch .information").text(d.parent.id);  #D
      select("#info .info-family .information")                     #D
      ?   .text(d.parent.data.parent);                             #D
      select("#info .info-total-speakers .information")             #D
      ?      .text(format(".3s")(d.data.total_speakers));             #D
      select("#info .info-native-speakers     .information")        #D
      ?      .text(format(".3s")(d.data.native_speakers));            #D
 
      select("#instructions").classed("hidden", true);  #E
      select("#info").classed("hidden", false);         #E
 
    })
    .on("mouseleave", ()=> {  #F
 
      select("#instructions").classed("hidden", false);  #G
      select("#info").classed("hidden", true);           #G
 
    });
   
};

当您将鼠标移到语言节点上时,您现在应该会看到有关分支、系列和说话人数量的其他信息显示在可视化效果的右侧,如图 11.9 所示。

图 11.9 当我们将鼠标移到语言节点上时,有关语言名称、分支、家族和说话者数量的信息将显示在可视化的右侧。



圆形包的一个缺点是它们很难在移动屏幕上呈现。尽管圆圈仍然在小屏幕上提供了父子关系的良好概述,但标签变得更加难以阅读。此外,由于语言圈可能会变小,因此使用触摸事件显示信息可能会很棘手。为了解决这些缺点,我们可以将语言家族相互堆叠,或者在移动设备上选择不同类型的可视化。

11.3 构建树形图

可视化父子关系的一种熟悉且直观的方法是使用树形图。树形图类似于家谱树。像圆形包一样,它们由节点组成,但也显示了它们之间的链接。在本节中,我们将构建 100 种最常用的语言的树形图,如图 11.10 所示。左侧是根节点,即“语言”。它分为语系,也细分为语言分支,最后是语言。我们用圆圈的大小可视化每种语言的使用者总数。至于圆圈包,这些圆圈的颜色代表它们所属的语言家族。

图 11.10 我们将在本节中构建的树形图的部分视图。



11.3.1 生成树布局

与上一节中构建的圆形包类似,D3树形图是使用布局生成器d3.tree()创建的,它是d3层次结构模块(https://github.com/d3/d3-hierarchy)的一部分。然后,我们使用布局提供的信息来绘制链接和节点。

图 11.11 要使用 D3 创建树形图,我们从分层数据(通常称为 root)开始。为了计算布局,我们将根数据结构传递给 D3 的 tree() 布局生成器。此生成器将每个节点的位置附加到数据中。最后,我们将链接、节点和标签附加到 SVG 容器,并使用附加的数据设置它们的位置和大小。


要生成树布局,让我们首先创建一个新文件并将其命名为 tree.js .在这个文件中,我们创建了一个名为drawTree()的函数,它将分层数据(也称为根,后代和叶)作为参数。在示例 11.15 中,我们声明了图表的维度。我们给它一个 1200px 的宽度,图表的 HTML 容器的宽度,以及 3000px 的高度。请注意,高度与图表中的叶节点数成正比,并且是通过反复试验找到的。处理树可视化时,请从大致值开始,并在可视化显示在屏幕上后进行调整。

为了生成布局,我们调用 D3 的 tree() 函数,我们从文件顶部的 d3-hierarchy 导入该函数,并设置其 size() 访问器函数,该函数将图表的宽度和高度数组作为参数。因为我们希望我们的树从左到右展开,所以我们首先传递 innerHeight,然后是 innerWidth 。如果我们希望树从上到下部署,我们会做相反的事情。最后,我们将分层数据(根)传递给树布局生成器。

示例 11.15 生成树布局(树.js)

import { tree } from "d3-hierarchy";
 
export const drawTree=(root, descendants, leaves)=> {
 
  const width=1200;                                         #A
  const height=3000;                                        #A
  const margin={top:60, right: 200, bottom: 0, left: 100};  #A
  const innerWidth=width - margin.left - margin.right;      #A
  const innerHeight=height - margin.top - margin.bottom;    #A
 
  const treeLayoutGenerator=tree()   #B
    .size([innerHeight, innerWidth]);  #B
  treeLayoutGenerator(root);  #C
 
};

在main.js中,我们还需要导入drawTree()函数并将根、后代和叶作为参数传递。

import { drawTree } from "./tree.js";
 
drawTree(root, descendants, leaves);

11.3.2 绘制树形图

生成布局后,绘制树形图非常简单。像往常一样,我们首先需要附加一个 SVG 容器并设置其 viewBox 属性。在示例 11.16 中,我们将这个容器附加到 div 中,其 id 为 “tree”,该 id 已存在于 index.html 中。请注意,我们必须从文件顶部的 d3-select 模块导入 select() 函数。我们还将一个 SVG 组附加到此容器,并根据前面定义的左边距和上边距进行转换,遵循自第 4 章以来使用的策略。

要创建链接,我们需要 d3.link() 链接生成器函数。此函数的工作方式与第 3 章中介绍的线路生成器完全相同。它是 d3 形状模块 (https://github.com/d3/d3-shape) 的一部分,我们使用命令安装它 npm 安装 d3 形状 .在文件的顶部,我们从 d3-shape 导入 link() 函数,以及 curveBumpX() 函数,我们将使用它来确定链接的形状。

然后我们声明一个名为 linkGenerator 的链接生成器,它将曲线函数 curveBumpX 传递给 D3 的 link() 函数。我们将它的 x() 和 y() 访问器函数设置为使用树布局生成器存储在 y 和 x 键中的值。就像我们准备树布局生成器时一样,x 和 y 值是反转的,因为我们希望从右到左而不是从上到下绘制树。

为了绘制链接,我们使用数据绑定模式从 root.links() 提供的数据中附加路径元素。此方法返回树的链接数组及其源点和目标点。然后调用链接生成器来计算每个链接或路径的 d 属性。最后,我们设置链接的样式并将其不透明度设置为 60%。

示例 11.16 绘制链接(树.js)

...
import { select } from "d3-selection";
import { link, curveBumpX } from "d3-shape";
 
export const drawTree=(root, descendants)=> {
 
  ...
 
  const svg=select("#tree")                                     #A
    .append("svg")                                                     #A
      .attr("viewBox", `0 0 ${width} ${height}`)                       #A
    .append("g")                                                       #A
      .attr("transform", `translate(${margin.left}, ${margin.top})`);  #A
 
  const linkGenerator=link(curveBumpX)  #B
    .x(d=> d.y)                          #B
    .y(d=> d.x);                         #B
  svg                                    #C
    .selectAll(".tree-link")             #C
    .data(root.links())                  #C
    .join("path")                        #C
      .attr("class", "tree-link")        #C
      .attr("d", d=> linkGenerator(d))  #C
      .attr("fill", "none")              #C
      .attr("stroke", "grey")            #C
      .attr("stroke-opacity", 0.6);      #C
 
};

准备就绪后,您的链接将类似于图 11.12 中的链接。请注意,此图仅显示部分视图,因为我们的树非常高!

图 11.12 树形图链接的部分视图。



为了突出显示每个节点的位置,我们将在树形图中附加圆圈。带有灰色笔划的小圆圈将表示根、语言家族和语言分支节点。相反,语言节点圆圈的大小与说话者总数成比例,并且具有与其语言家族关联的颜色。

要计算语言节点的大小,我们需要一个规模。在清单 11.17 中,我们转到 scales.js并从 d3-scale 导入 scaleRadial()。量表的域是连续的,从零扩展到数据集中说其中一种语言的最大人数。它的范围可以在 83 到 <>px 之间变化,这是上一节中创建的圆包中最大圆的半径。

因为最大说话人数只有在我们检索数据并创建层次结构(根)后才可用,我们需要将径向刻度包装到一个名为 getRadius() 的函数中。当我们需要计算圆的半径时,我们将传递当前语言的说话者数量以及最大说话者数量,此函数将返回半径。

示例 11.17 创建一个函数来从径向刻度(scales.js)中检索值

import { scaleOrdinal, scaleRadial } from "d3-scale";
...
 
export const getRadius=(maxSpeakers, speakers)=> {
  const radialScale=scaleRadial()
    .domain([0, maxSpeakers])
    .range([0, 83]);
 
  return radialScale(speakers);
};

回到树.js ,我们用方法 d3.max() 计算最大扬声器数。要使用这种方法,我们需要安装 d3-array 模块 (https://github.com/d3/d3-array) 与 npm install d3-array ,并在文件顶部导入 max() 函数。我们还从 scales 导入函数 getRadius() 和色阶.js .

然后,我们使用数据绑定模式将一个圆附加到每个后代节点的内部图表中。我们使用树布局生成器附加到数据中的 x 和 y 键来设置这些圆圈的 cx 和 cy 属性。如果圆是一个叶节点,我们根据相关语言的说话者数量和 getRadius() 函数设置其半径。我们使用色阶设置其颜色,填充不透明度为 30%,描边设置为“无”。其他圆圈的半径为 4px,白色填充和灰色描边。

示例 11.18 追加节点(树.js)

...
import { max } from "d3-array";
import { getRadius, colorScale } from "./scales";
 
export const drawTree=(root, descendants)=> {
  ...
 
  const maxSpeakers=max(leaves, d=> d.data.total_speakers);  #A
  svg                              #B
    .selectAll(".node-tree")       #B
    .data(descendants)             #B
    .join("circle")                #B
      .attr("class", "node-tree")  #B
      .attr("cx", d=> d.y)        #B
      .attr("cy", d=> d.x)        #B
      .attr("r", d=> d.depth===3                      #C
        ? getRadius(maxSpeakers, d.data.total_speakers)  #C
        : 4                                              #C
      )                                                  #C
      .attr("fill", d=> d.depth===3      #D
        ? colorScale(d.parent.data.parent)  #D
        : "white"                           #D
      )                                     #D
      .attr("fill-opacity", d=> d.depth===3  #E
        ? 0.3                                   #E
        : 1                                     #E
      )                                         #E
      .attr("stroke", d=> d.depth===3        #E
        ? "none"                                #E
        : "grey"                                #E
      );                                        #E
 
};

为了完成树形图,我们为每个节点添加一个标签。在示例 11.19 中,我们使用数据绑定模式为数据集中的每个节点附加一个文本元素。如果标签与叶节点相关联,则在右侧显示标签。否则,标签将位于其节点的左侧。我们还为标签提供白色笔触,以便它们在放置在链接上时更易于阅读。通过将绘制顺序属性设置为“描边”,我们可以确保在文本填充颜色之前绘制描边。这也有助于提高可读性。

示例 11.19 为每个节点附加一个标签(树.js)

export const drawTree=(root, descendants)=> {
 
  ...
 
svg
  .selectAll(".label-tree")       #A
  .data(descendants)              #A
  .join("text")                   #A
    .attr("class", "label-tree")  #A
    .attr("x", d=> d.children ? d.y - 8 : d.y + 8)          #B
    .attr("y", d=> d.x)                                     #B
    .attr("text-anchor", d=> d.children ? "end" : "start")  #B
    .attr("alignment-baseline", "middle")                    #B
    .attr("paint-order", "stroke")                         #C
    .attr("stroke", d=> d.depth===3 ? "none" : "white")  #C
    .attr("stroke-width", 2)                               #C
    .style("font-size", "16px")
    .text(d=> d.id);
 
};

完成后,您的树形图应类似于托管项目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/) 和图 11.13 中的树形图。

图 11.13 已完成树形图的部分视图



此树形图的线性布局使其相对容易地转换为移动屏幕,只要我们增加标签的字体大小并确保它们之间有足够的垂直空间。有关构建响应式图表的提示,请参阅第 9 章。

为了完成这个项目,我们需要为语言家族的颜色和圆圈的大小添加一个图例。我们已经为您构建了它。要显示图例,请转到索引.html然后取消注释带有“图例”类的 div。然后,在main.js中,从legend导入函数createLegend(.js并调用它来生成legend。请参阅第7章中的鲸目动物可视化,以获取有关我们如何构建这个传说的更多解释。看看图例中的代码.js甚至更好的是,尝试自己构建它!

11.4 构建其他分层可视化

在本章中,我们讨论了如何构建圆形包和树形图可视化。使用 D3 制作其他层次结构表示形式(如树状图和冰柱图)非常相似。

图 11.14 说明了我们如何从 CSV 或 JSON 数据开始,我们将其格式化为名为 root 的分层数据结构。使用这种数据结构,我们可以构建任何分层可视化,唯一的区别是用作布局生成器的功能。对于圆形包,布局生成器函数为 d3.pack() ;对于树状图,d3.treemap() ;对于树形图,d3.tree() ;对于冰柱图,d3.partition() 。我们可以在 d3 层次结构模块 (https://github.com/d3/d3-hierarchy) 中找到这些布局生成器。

图 11.14 要使用 D3 创建分层可视化,我们首先将 CSV 或 JSON 数据格式化为称为 root 的分层数据结构。从这个根,我们可以生成一个圆形包、一个树状图、一个树形图或一个冰柱图。每个图表都有自己的布局生成器,用于计算布局并将其追加到数据集中所需的信息。然后,我们使用这些信息来绘制我们需要的形状:圆形、矩形或链接。


练习:创建树状图

现在,您已经掌握了构建 100 种最常用的语言的树状图所需的所有知识,如下图所示,以及托管项目 (https://d3js-in-action-third-edition.github.io/100-most-spoken-languages/)。树状图将分层数据可视化为一组嵌套矩形。传统上,树状图仅显示叶节点,在我们的例子中,叶节点是语言。矩形或叶节点的大小与每种语言的使用者总数成正比。

1. 在索引中.html添加一个 id 为“树状图”的 div。

2. 使用一个名为 drawTreemap() 的函数创建一个名为 treemap.js 的新文件。此函数接收根数据结构和叶作为参数,并从 main.js 调用。

3. 使用 d3.treemap() 布局生成器计算树状图布局。使用 size() 访问器函数,设置图表的宽度和高度。您还可以使用填充Inner()和paddingOuter()指定矩形之间的填充。有关更深入的文档 (https://github.com/d3/d3-hierarchy),请参阅 d3 层次结构模块。

4. 将 SVG 容器附加到 div,ID 为“树状图”。

5. 为每个叶节点附加一个矩形。使用布局生成器添加到数据集的信息设置其位置和大小。

6. 在相应的矩形上附加每种语言的标签。您可能还希望隐藏显示在较小矩形上的标签。

图 11.15 100 种最常用的语言的树状图


如果您在任何时候遇到困难或想将您的解决方案与我们的解决方案进行比较,您可以在附录 D 的 D.11 节和文件夹 11.4-树状图/本章代码文件的末尾找到它。但是,像往常一样,我们鼓励您尝试自己完成它。您的解决方案可能与我们的略有不同,没关系!

11.5 小结

  • 分层可视化通过外壳连接邻接来传达父子关系。
  • 可以从 CSV 或 JSON 数据创建它们。
  • 如果我们使用 CSV 数据,我们需要两列:一列用于子节点,另一列用于其父节点。一旦数据集加载到我们的项目中,我们使用方法 d3.stratify() 生成一个名为 root 的分层数据结构。
  • 如果我们使用分层 JSON 数据,我们使用方法 d3.hierarchy() 生成名为 root 的分层数据结构。
  • 根分层数据结构有一个返回所有节点数组的 descendants() 方法和一个返回所有叶节点(没有子节点)的数组的 leaves() 方法。
  • 为了创建圆形包可视化,我们将根分层数据传递给布局生成器 d3.pack() 。此生成器计算每个圆圈的位置和大小,并将此信息附加到数据集中。然后,我们使用它来绘制构成可视化的圆圈。
  • 为了绘制树形图,我们将根分层数据传递给布局生成器 d3.tree()。此生成器计算每个节点的位置,并将此信息附加到数据集中。然后,我们使用它来绘制构成可视化的链接和圆圈。