龙空技术网

D3.js实战教程:11 分层可视化

启辰8 93

前言:

今天你们对“js获取节点高度”大体比较讲究,大家都想要学习一些“js获取节点高度”的相关文章。那么小编也在网上网罗了一些关于“js获取节点高度””的相关内容,希望大家能喜欢,朋友们一起来学习一下吧!

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

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

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

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

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

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

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

在本章中,我们将构建两个分层可视化:圆形包和线性树形图。我们将基于世界上 100 种使用最多的语言的数据集进行可视化。您可以看到我们将在 构建的图表。

我们数据集中的每种语言都属于一个语言家族或一组从共同祖先发展而来的相关语言。这些家族可以细分为称为分支的较小组。让我们以五种最常用的语言为例。在表 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(),这是一个非常易于使用且需要接近零的配置的捆绑器。

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

npm install -g parcel

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

在您的计算机上安装 Parcel 后,在代码编辑器 () 中打开本章代码文件的起始文件夹。如果您使用的是 VS Code,请打开集成终端并运行命令 npm install 以安装项目依赖项。在此阶段,我们唯一的依赖项是允许我们稍后加载CSV数据文件。

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

parcel src/index.html

在浏览器中打开 以查看您的项目。每次保存文件时,浏览器中显示的项目都会自动更新。完成工作会话后,您可以通过输入终端 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 模块 () 包含一个名为 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 选择模块 ()。此模块包含负责操作 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 缩放模块 () 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-插值模块 () 中可用。使用 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 和托管项目 () 中的包所示。现在,我们可以在可视化中找到主要语言。

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

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

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

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

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

为了区分节点级别,在清单 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层次结构模块()的一部分。然后,我们使用布局提供的信息来绘制链接和节点。

图 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 形状模块 () 的一部分,我们使用命令安装它 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 模块 () 与 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); };

完成后,您的树形图应类似于托管项目 () 和图 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 层次结构模块 () 中找到这些布局生成器。

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

练习:创建树状图

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

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

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

3. 使用 d3.treemap() 布局生成器计算树状图布局。使用 size() 访问器函数,设置图表的宽度和高度。您还可以使用填充Inner()和paddingOuter()指定矩形之间的填充。有关更深入的文档 (),请参阅 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()。此生成器计算每个节点的位置,并将此信息附加到数据集中。然后,我们使用它来绘制构成可视化的链接和圆圈。

标签: #js获取节点高度 #js表格文字居中 #js随机指定范围查询