在上一章中,我们讨论了 D3 如何使用其形状生成器函数计算复杂形状(如曲线、面积和弧)的 d 属性。在本章中,我们将通过布局将这些形状提升到另一个层次。在 D3 中,布局是将数据集作为输入并生成新的批注数据集作为输出的函数,其中包含绘制特定可视化效果所需的属性。例如,饼图布局计算饼图每个扇区的角度,并使用这些角度批注数据集。同样,堆栈布局计算堆积形状在堆积条形图或流图中的位置。
布局不会绘制可视化效果,也不会像组件一样调用它们,也不会像形状生成器那样在绘图代码中引用。相反,它们是一个预处理步骤,用于设置数据的格式,以便准备好以您选择的形式显示。
图5.1 布局功能是用于计算绘制特定图表所需信息的数据预处理步骤。
在本章中,我们将饼图和堆栈布局与第 4 章中讨论的弧形和面积形状生成器相结合,以创建图 5.2 所示的项目。您也可以在 https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/ 在线找到它。该项目可视化了 1973 年至 2019 年间音乐行业每种格式的销售情况。它的灵感来自2020年MakeoverMonday(www.makeovermonday.co.uk/week-21-2020/)举办的挑战。
图 5.2 1973 年至 2019 年音乐行业销售的可视化。这是我们将在本章中构建的项目。
虽然本章只介绍了饼图和堆栈布局,但其他布局,如和弦布局和更奇特的布局,遵循相同的原则,看完这些应该很容易理解。
在开始之前,请转到第 5 章的代码文件。您可以从本书的 Github 存储库下载它们(https://github.com/d3js-in-action-third-edition/code-files)。在名为 chapter_05 的文件夹中,代码文件按节进行组织。要开始本章的练习,请在代码编辑器中打开 5.1-Pie_layout/start 文件夹并启动本地 Web 服务器。如果您需要有关设置本地开发环境的帮助,请参阅附录 A。您可以在位于本章代码文件根目录下的自述文件中找到有关项目文件夹结构的更多详细信息。
我们将在本章中构建的三个可视化(圆环图、堆积条形图和流图)共享相同的数据、维度和比例。为了避免重复,该项目被分解为多个 JavaScript 文件,其中一个用于可视化共享的常量,另一个专门用于比例。这种方法将使我们的代码更易于阅读和修改。在生产代码中,我们可能会使用 JavaScript 导入和导出来访问不同的函数,并结合 Node 和捆绑器。在讨论前端框架时,我们将到达那里,但现在,我们将坚持一个类似遗留的项目结构,以保持对 D3 的关注。请注意,D3 库和所有 JavaScript 文件都已加载到 index.html 中。
警告使用本章的代码文件时,在代码编辑器中仅打开一个开始文件夹或一个结束文件夹。如果一次打开章节的所有文件并使用 Live Server 扩展为项目提供服务,则数据文件的路径将无法按预期工作。
5.1 创建饼图和圆环图在本节中,我们将使用 D3 的饼图布局来创建圆环图,您可以在图 5.2 顶部和托管项目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上看到该圆环图。更具体地说,我们将可视化 1975 年、1995 年和 2013 年每种音乐格式的销售额细分。每个圆环图的中心将对应于相应年份在流图和下面堆叠条形图的 x 轴上的位置。
5.1.1 准备步骤让我们花点时间建立一个策略,以确保每个图表根据 x 轴上的年份正确水平对齐。一个简单的方法是使用第4章中描述的保证金约定。随着本章的进展,我们将使用三个 SVG 容器:一个用于圆环图,一个用于流图,一个用于堆积条形图。这些容器中的每一个都具有相同的尺寸并共享相同的边距。为内部图表保留的区域(没有轴和标签的可视化效果)也将具有相同的维度并水平对齐,如图 5.3 所示。文件 js/shared-constant.js 已包含可视化共享的边距对象和维度常量。
我们还在 js/load-data 中为您加载了 CSV 数据文件.js .有关如何将数据加载到 D3 项目中的更多信息,请参阅第 4 章和第 3 章。加载数据后,我们调用函数 defineScales() 和 drawDonutCharts() ,我们将在本节中使用它们。
图 5.3 为了创建本章的项目,我们将使用三个 SVG 容器:一个用于圆环图,一个用于流线图,一个用于堆积条形图。此策略将使我们能够为内部图表保留一致的区域,并将每个图表正确对齐到另一个图表之上。首先,让我们为圆环图追加一个 SVG 容器和一个定义为内部图表保留区域的 SVG 组。为此,我们转到 js/donut-charts.js并在函数 drawDonutCharts() 中,我们创建 SVG 容器和一个 SVG 组。在下面的代码片段中,您将看到我们在 div 内附加了 SVG 容器,ID 为 donut 。请注意,我们通过根据图表的左边距和上边距平移组来应用边距约定。
const svg = d3.select("#donut")
.append("svg") #A
.attr("viewBox", `0 0 ${width} ${height}`); #A
const donutContainers = svg
.append("g") #B
.attr("transform", `translate(${margin.left}, ${margin.top})`); #B
您可能想知道为什么我们需要将边距约定应用于圆环图,因为没有轴和标签的帐户空间。这是因为每个圆环图将根据其所代表的年份水平定位。由于我们希望这些年份的水平位置与下面的流线图和堆叠条形图中相同,因此我们需要考虑边际惯例。
在第 4 章中,我们讨论了极坐标以及如何通过将弧包含在 SVG 组中并将该组转换为图表中心的位置来促进饼图或圆环图的创建。通过以这种方式进行,弧线将自动围绕该中心绘制。
我们将在这里应用相同的策略,唯一的区别是我们需要考虑三个圆环图,并且它们的中心水平位置对应于它们所代表的年份,如图 5.4 所示。
图 5.4 组成圆环图的每组弧都包含在 SVG 组中。这些组根据它们所代表的年份进行水平翻译。该位置是使用 D3 刻度计算的。
要计算每个甜甜圈中心的水平位置,我们需要一个刻度。如您所知,我们使用 D3 刻度将数据(此处为年份)转换为屏幕属性,此处为水平位置。线性或时间刻度对于我们的目的来说效果很好,但我们选择波段刻度,因为我们知道我们稍后会绘制一个堆叠条形图,它将共享相同的刻度。有关频段刻度工作原理的更多说明,请参阅第 3 章。
在文件中 js/scale.js ,我们首先使用函数 d3.scaleBand() 初始化波段刻度,并将其存储在名为 xScale 的常量中。请注意我们如何在函数 defineScales() 中声明刻度的域和范围。这种方法让我们等到数据加载完成,然后再尝试使用它来设置域(一旦数据准备就绪,函数 defineScales() 从加载数据调用.js)。我们在函数外部声明常量 xScale,使其可以从其他 js 文件访问。
示例 5.1 声明波段刻度(scales.js)const xScale = d3.scaleBand(); #A
const defineScales = (data) => {
xScale
.domain(data.map(d => d.year)) #B
.range([0, innerWidth]); #B
};
带状刻度接受离散输入作为域,并返回该范围的连续输出。在清单 5.1 中,我们使用 JavaScript map() 方法,通过每年从数据集创建一个数组来设置域。对于范围,我们传递一个数组,其中包含可用水平空间的最小值(零)和最大值(对应于内部图表的 innerWidth)。
我们回到函数 drawDonutCharts() ,正如你在清单 5.2 中看到的,我们首先声明一个名为 years 的数组,它列出了我们感兴趣的年份,这里是 1975、1995 和 2013。然后,使用 forEach() 循环,我们为感兴趣的每一年附加一个 SVG 组,并将其保存在名为 donutContainer 的常量中。最后,我们通过设置组的转换属性来翻译组。水平平移是通过调用计算的 xScale ,我们将当前年份传递到该平移,而垂直平移对应于内部图表的半高。
示例 5.2 为每个圆环图追加和翻译一个 SVG 组(圆环图.js)const years = [1975, 1995, 2013];
years.forEach(year => {
const donutContainer = donutContainers
.append("g")
.attr("transform", `translate(${xScale(year)}, ${innerHeight/2})`);
});
5.1.2 饼图布局生成器
完成准备步骤后,我们现在可以专注于圆环图。饼图和圆环图可视化部分与整体的关系或每个扇区相对于总量表示的数量。D3 饼图布局生成器通过根据每个切片所代表的百分比计算每个切片的开始和结束角度来帮助我们。
设置数据格式D3 的饼图生成器希望输入数据格式化为数字数组。例如,对于 1975 年,我们可以有一个数组,其中包含与每种音乐格式对应的销售额,如下所示:
const sales1975 = [8061.8, 2770.4, 469.5, 0, 0, 0, 48.5];
虽然这样一个简单的数组足以生成饼图,但它会阻止我们以后根据它所代表的音乐格式为每个切片分配颜色。为了随身携带这些信息,我们可以使用一个对象数组,其中包含音乐格式的 ID 和感兴趣年份的相关销售额。
在示例 5.3 中,我们首先从加载数据集的 columns 属性中提取格式。获取数据时,例如,使用 d3.csv() 方法,D3 将一个数组附加到数据集,其中包含原始 CSV 数据集中每列的标题,并使用键 data.columns 进行访问。如果将提取的数据记录到控制台中,则会在数据数组的末尾看到它,如图 5.5 所示。
由于我们只对音乐格式感兴趣,因此我们可以过滤列数组以删除“year”标签。
图 5.5 从 CSV 文件获取数据时,D3 将数组附加到数据集,其中包含原始数据集中列的标题。可以使用键 data.columns 访问此数组。
为了准备饼图生成器的数据,我们还需要提取感兴趣的年份的数据。我们使用 JavaScript 方法 find() 隔离这些数据,并将其存储在名为 yearData 的常量中。
我们遍历格式数组,对于每种格式,我们创建一个对象,其中包含格式 id 及其感兴趣年份的相关销售额。最后,我们将这个对象推入 数组格式化数据 ,之前声明。
示例 5.3 设置饼图生成器的数据格式(圆环图.js)const years = [1975, 1995, 2013];
const formats = data.columns.filter(format => format !== "year"); #A
years.forEach(year => {
...
const yearData = data.find(d => d.year === year); #B
const formattedData = []; #C
formats.forEach(format => { #D
formattedData.push({ format: format, sales: yearData[format] }); #D
}); #D
});
准备就绪后,格式化数据是一个对象数组,每个对象都包含格式的 id 及其感兴趣年份的相关销售额。
// => formattedData = [
{ format: "vinyl", sales: 8061.8 },
{ format: "eight_track", sales: 2770.4 },
{ format: "cassette", sales: 469.5 },
{ format: "cd", sales: 0 },
{ format: "download", sales: 0 },
{ format: "streaming", sales: 0 },
{ format: "other", sales: 48.5 }
];
初始化和调用饼图布局生成器
现在数据格式正确,我们可以初始化饼图布局生成器。我们用方法 d3.pie() 构造一个新的饼图生成器,它是 d3 形状模块 (https://github.com/d3/d3-shape#pies) 的一部分。由于格式化数据是一个对象数组,我们需要告诉饼图生成器哪个键包含将决定切片大小的值。我们通过设置 value() 访问器函数来做到这一点,如以下代码片段所示。我们还将 pie 生成器存储在一个名为 pieGenerator 的常量中,以便我们可以像调用任何其他函数一样调用它。
const pieGenerator = d3.pie()
.value(d => d.sales);
要生成饼图布局的数据,我们只需调用饼图生成器函数,将格式化的数据作为参数传递,并将结果存储在名为 注释数据 .
const pieGenerator = d3.pie()
.value(d => d.sales);
const annotatedData = pieGenerator(formattedData);
饼图生成器返回一个新的带批注的数据集,其中包含对原始数据集的引用,但也包括新属性:每个切片的值、其索引及其开始和结束角度(以弧度为单位)。请注意,每个切片之间的填充也包括 padAngle 并且当前设置为零。我们稍后会改变这一点。
// => annotatedData = [
{
data: { format: "vinyl", sales: 8061.8 },
value: 8061.8,
index: 0,
startAngle: 0,
endAngle: 4.5,
padAngle: 0,
},
...
];
请务必了解饼图布局生成器不直接参与绘制饼图。这是一个预处理步骤,用于计算饼图扇区的角度。如图5.1和5.6所述,此过程通常包括三个步骤:
图 5.6 饼图布局生成器是一个预处理步骤,用于生成一个带注释的数据集,其中包含饼图每个切片的开始和结束角度。该过程通常涉及格式化我们的数据,初始化饼图生成器函数,并调用该函数以获取带注释的数据。
5.1.3 绘制圆弧准备好带注释的数据集后,是时候生成弧线了!您将看到以下步骤与上一章中创建弧的方式非常相似。出于这个原因,我们不会解释每一个细节。如果您需要更深入的讨论,请参阅第 4 章。
在示例 5.4 中,我们首先通过调用 d3.arc() 方法及其负责设置图表内外半径、切片之间的填充以及切片角半径的各种访问器函数来初始化 arc 生成器。如果内半径设置为零,我们将获得一个饼图,而如果它大于零,我们将得到一个圆环图。
与第 4 章中使用的策略的唯一区别是,这次我们可以在声明电弧发生器的同时设置 startAngle() 和 endAngle() 访问器函数。这是因为现在,这些值包含在带注释的数据集中,我们可以告诉这些访问器函数如何通过 d.startAngle 和 d.endAngle .
要使弧出现在屏幕上,我们需要做的最后一件事是使用数据绑定模式为注释数据集中的每个对象生成一个路径元素(每个弧或切片都有一个对象)。请注意,在清单 5.4 中,我们如何为每个甜甜圈的弧指定一个特定的类名 ( 'arc-${year}' ),并将该类名用作数据绑定模式中的选择器。由于我们正在循环中创建甜甜圈,这将防止 D3 在制作新甜甜圈时覆盖每个甜甜圈。
最后,我们调用弧发生器函数来计算每条路径的 d 属性。
示例 5.4 生成和绘制圆弧(圆环图.js)const arcGenerator = d3.arc()
.startAngle(d => d.startAngle) #A
.endAngle(d => d.endAngle) #A
.innerRadius(60)
.outerRadius(100)
.padAngle(0.02)
.cornerRadius(3);
const arcs = donutContainer
.selectAll(`.arc-${year}`) #B
.data(annotatedData) #B
.join("path") #B
.attr("class", `arc-${year}`)
.attr("d", arcGenerator); #C
使用色阶
如果您保存项目并在浏览器中查看圆环图,您会发现它们的形状是正确的,但每个弧线都是漆黑的。这是正常的,黑色是 SVG 路径的默认填充属性。为了提高可读性,我们将根据每个弧线所代表的音乐格式对它们应用不同的颜色。
将正确的颜色应用于每个弧的一种简单且可重用的方法是声明色阶。在 D3 中,色阶通常使用 d3.scaleOrdinal() (https://github.com/d3/d3-scale#scaleOrdinal) 创建。序数刻度将离散域映射到离散范围。在我们的例子中,域是音乐格式的数组,范围是包含与每种格式关联的颜色的数组。
在文件比例中.js ,我们首先声明一个序数比例并将其保存在常量色阶中。然后,我们通过将 formatInfo 数组(在共享常量中可用.js的每个格式 id 映射到数组中来设置其域。我们对颜色做同样的事情,您可以根据自己的喜好进行个性化设置。在本章中,我们将重用此色阶来创建构成我们项目的所有图表。
const colorScale = d3.scaleOrdinal();
const defineScales = (data) => {
colorScale
.domain(formatsInfo.map(f => f.id))
.range(formatsInfo.map(f => f.color));
};
回到圆环图.js我们可以通过将绑定到每个弧的音乐格式 id 传递给色阶来设置弧的填充属性。
const arcs = donutContainer
.selectAll(`.arc-${year}`)
.data(annotatedData)
.join("path")
.attr("class", `arc-${year}`)
.attr("d", arcGenerator)
.attr("fill", d => colorScale(d.data.format));
保存您的项目并在浏览器中查看。看起来还不错!弧线已按降序显示,从最大到最小,这有助于提高可读性。我们已经可以看到音乐的面貌在 1975 年、1995 年和 2013 年间发生了怎样的变化,主导格式完全不同。
图 5.7 1975年、1995年和2013年的圆环图
5.1.4 添加标签在第4章中,我们提到饼图有时很难解释,因为人脑不太擅长将角度转换为比率。我们可以通过在圆环图的质心上添加一个标签来提高圆环图的可读性,该标签以百分比表示每个弧的值,就像我们在上一章中所做的那样。
在示例 5.5 中,我们稍微修改了用于创建圆弧的代码(来自示例 5.4)。首先,我们使用数据绑定模式来追加 SVG 组而不是路径元素。然后,我们将路径元素(用于圆弧)和 SVG 文本元素(用于标签)附加到这些组中。由于父母将绑定数据传递给孩子,因此我们将在塑造弧线和标签时访问数据。
我们通过调用电弧发生器来绘制电弧,就像我们之前所做的那样。要设置标签的文本,我们需要计算每个弧线表示的比率或百分比。我们通过从弧的结束角度中减去弧的起始角并将结果除以 2π(以弧度为单位的完整圆覆盖的角度)来执行此计算。请注意我们如何使用括号表示法(d[“百分比”])将百分比值存储到绑定数据中。当我们需要对不同的属性进行相同的计算时,这个技巧很有用。它可以防止您多次重复计算。为了返回标签的文本,我们将计算出的百分比传递给方法 d3.format(“.0%”) ,该方法生成一个舍入百分比并在标签末尾添加一个百分比符号。
我们应用相同的策略来计算每个弧的质心,这是我们想要放置标签的位置。当设置标签的 x 属性时,我们计算相关弧的质心(使用第 4 章中讨论的技术)并将其存储在绑定数据中( d[“质心”])。然后,在设置 y 属性时,质心数组已经可以通过 d.centroid 访问。
为了使标签水平和垂直地以质心居中,我们需要将其文本锚点和主要基线属性设置为中间。我们还使用fill属性将它们的颜色设置为白色,将其字体大小增加到16px,将其字体粗细增加到500以提高可读性。
如果您保存项目并在浏览器中查看圆环图,您会发现标签在大弧上工作良好,但在较小的弧线上几乎无法读取。在专业项目中,我们可以通过将小弧的标签移动到圆环图之外来解决这个问题。对于此项目,当百分比小于 5% 时,我们根本不会通过将其填充不透明度属性设置为零来显示这些标签。
示例 5.5 在每个弧的质心上添加值标签(圆环图.js)const arcs = donutContainer
.selectAll(`.arc-${year}`)
.data(annotatedData)
.join("g") #A
.attr("class", `arc-${year}`);
arcs #B
.append("path") #B
.attr("d", arcGenerator) #B
.attr("fill", d => colorScale(d.data.format)); #B
arcs
.append("text") #C
.text(d => {
d["percentage"] = (d.endAngle - d.startAngle) / (2 * Math.PI); #D
return d3.format(".0%")(d.percentage); #D
})
.attr("x", d => { #E
d["centroid"] = arcGenerator #E
.startAngle(d.startAngle) #E
.endAngle(d.endAngle) #E
.centroid(); #E
return d.centroid[0]; #E
}) #E
.attr("y", d => d.centroid[1]) #E
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "#f6fafc")
.attr("fill-opacity", d => d.percentage < 0.05 ? 0 : 1) #F
.style("font-size", "16px")
.style("font-weight", 500);
图 5.8 带百分比标签的圆环图
作为最后一步,我们将指示圆环图所代表的年份,标签位于其中心。我们通过在每个甜甜圈容器中附加一个文本元素来做到这一点。因为我们还在循环往复年份,所以我们可以直接应用当前年份作为标签的文本。此外,由于圆环容器位于图表的中心,因此文本元素会自动正确定位。我们所要做的就是设置其文本锚点和主要基线属性,使其水平和垂直居中。
donutContainer
.append("text")
.text(year)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "24px")
.style("font-weight", 500);
瞧!我们的圆环图是完整的。
图 5.9 带有年份标签的完整圆环图
图 5.10 回顾了创建饼图或圆环图的步骤。在第一步中,我们使用布局函数 d3.pie() 预处理数据,以获得带有注释的数据集,其中包含每个切片的角度。然后,我们使用弧发生器函数绘制弧线,该函数从注释数据集中获取角度并返回每个路径的 d 属性。最后,我们使用 SVG 文本元素添加标签以提高图表的可读性。
图 5.10 创建饼图或圆环图所涉及的主要步骤。
5.2 堆叠形状到目前为止,我们已经处理了可以在任何传统电子表格中轻松创建的信息可视化的简单示例。但是你进入这个行业并不是为了制作类似Excel的图表。您可能希望用漂亮的数据让您的观众惊叹不已,为您的美学 je ne sais quoi 赢得奖项,并通过您随着时间的推移而变化的表现唤起深刻的情感反应。
流图是代表变化和变化的崇高信息可视化。在你开始把这些部分放在一起之前,创作似乎具有挑战性。归根结底,流图是所谓的堆积面积图的变体。这些层相互累積,并根据靠近中心的组件所占用的空间来调整上方和下方元素的面积。它似乎是有机的,因为这种吸积性模仿了许多生物的生长方式,似乎暗示了控制生物生长和衰败的各种涌现特性。我们稍后会解释它的外观,但首先,让我们弄清楚如何构建它。
我们在本书的第一部分看了一个流线图,因为它实际上并没有那么奇特。流图是一种堆积图,这意味着它与堆积条形图基本相似,如图 5.11 所示。流线图也类似于我们在上一章中构建的折线图后面的区域,只是这些区域相互堆叠。在本节中,我们将使用 D3 的堆栈和面积生成器来创建堆叠条形图,然后创建流线图。
图 5.11 流图与堆积条形图基本相似。在 D3 中,两者都是使用堆栈布局生成器创建的。
在 D3 中,创建堆积条形图或流图的步骤类似,如图 5.12 所示。首先,我们初始化一个堆栈布局生成器并设置堆栈的参数。然后,我们将原始数据集传递给堆栈生成器,堆栈生成器将返回一个新的注释数据集,指示每个数据点的下限和上限。如果我们制作一个流线图,我们还必须初始化一个面积生成器,类似于上一章中讨论的直线和曲线生成器。最后,我们将带注释的数据集绑定到制作图表所需的 SVG 形状、堆叠条形图的矩形或流图的路径。在流图的情况下,调用面积生成器来计算路径的 d 属性。我们将在以下小节中更详细地介绍这些步骤。
图 5.12 使用 D3 创建堆积图的步骤。
5.2.1 堆栈布局生成器堆栈布局生成器是一个 D3 函数,它将具有多个类别的数据集作为输入。本章示例中使用的数据集包含 1973 年至 2019 年间每年不同音乐格式的总销售额。每种音乐格式将成为堆叠图表中的一个系列。
与前面讨论的饼图布局生成器一样,堆栈布局函数返回一个新的注释数据集,其中包含不同序列在“堆叠”到另一个时的位置。堆栈生成器是 d3 形状模块 (https://github.com/d3/d3-shape#stacks) 的一部分。
让我们将堆栈布局付诸行动,并开始在位于堆叠条形图中的函数 drawStackedBars() 中工作.js 。请注意,此函数已经包含将 SVG 容器附加到 div 的代码,ID 为 “bars”,以及内部图表的组容器。这与我们在第4章中使用的策略相同,与保证金惯例并行。
在下面的代码片段中,我们首先使用方法 d3.stack() 声明一个堆栈生成器,并将其存储在一个名为 stackGenerator 的常量中。然后,我们需要告诉生成器数据集中的哪些键包含我们要堆叠的值(将成为序列)。我们使用 keys() 访问器函数来做到这一点,我们将类别 id 数组传递给该函数,这里是每种音乐格式的标识符。我们通过映射 formatInfo 常量的 id 来创建这个数组。我们还可以使用附加到数据集的列键并过滤掉年份,就像我们在 5.1.2 节中所做的那样。
最后,我们调用堆栈生成器并将数据作为参数传递,以获得带注释的数据集。我们将新数据集存储在名为 注释数据 .
const stackGenerator = d3.stack() #A
.keys(formatsInfo.map(f => f.id)); #B
const annotatedData = stackGenerator(data); #C
如果将带注释的数据集记录到控制台中,您将看到它由多维数组组成。我们首先为每个系列提供一个数组,如图 5.13 所示,序列的 id 可通过 key 属性获得。然后,序列数组包含另一组数组,数据集中每年一个数组。最后这些数组包括相关年份类别的下限和上限以及该年份的原始数据。下限和上限分别由索引 d[0] 和 d[1] 访问,如果 d 对应于数组。
格式“乙烯基”是堆栈布局处理的第一个键。请注意,它的下限始终为零,而其上边界对应于该格式的当年销售额。然后,以下类别是“8 轨”。8 轨的下边界对应于黑胶唱片的上边界,我们将 8 轨的销量相加以获得其上限,从而创建一个堆栈。
图 5.13 堆栈布局生成器返回的带注释的数据集。
如果“堆栈”的概念还不清楚,下图可能会有所帮助。如果我们从原始数据集中仔细观察 1986 年,我们将看到音乐主要通过三种格式提供:黑胶唱片的销量为 2,825M$,盒式磁带为 5,830M$,CD 为 2,170M$。我们在图5.14的左侧显示了这些数据点,独立绘制。
当我们使用堆栈布局时,我们创建所谓的“数据列”而不是“数据点”,每列都有下限和上限。如果我们的堆栈从黑胶唱片开始,则下限为零,上边界对应于 1986 年黑胶唱片的销售额:2,825M$。然后,我们将盒式磁带的销售叠加在其上:下边界对应于黑胶唱片的上限(2,825M$),上边界是黑胶唱片和盒式磁带(8,655M$)的销售量。这个上边界成为CD销售的下限,其上边界对应于三种格式(10,825M$)的销售量相加。这些边界在带注释的数据集中通过索引(d[0]和d[1])访问。
图 5.14 堆栈布局生成器将数据点转换为堆叠数据列,并返回包含每个数据列的下限和上限的带注释的数据集。在这里,我们看到 1986 年的一个例子。
5.2.2 绘制堆积条形图在本节中,我们将创建您在图 5.11 底部看到的堆积条形图。堆积条形图类似于我们在第 2 章和第 3 章中已经制作的条形图,只是条形图分为多个类别或系列。堆积条形图和一般的堆积可视化通常用于显示趋势随时间推移的演变。
就像我们对圆环图所做的那样,我们将使用堆栈布局返回的带注释的数据集来绘制对应于每个类别的条形。但首先,我们需要一个垂直轴的比例,将每个矩形的下边界和上边界转换为垂直位置。我们希望条形的高度与销售额成线性比例,因此我们将使用线性刻度。由于此刻度需要访问带注释的数据,因此我们将在函数 drawStackedBars() 中声明它。
刻度域从零到注释数据中可用的最大上限。我们知道,这个最大值必须存在于最后一个带注释的数据系列中,这些数据将位于图表的顶部。我们可以使用 length 属性访问这个系列( annotatedData[annotatedData.length - 1])。然后,我们使用方法 d3.max() 检索属性 d[1] 下的最大值,该值对应于上限。
垂直刻度的范围从内部图表底部的innerHeight到内部图表顶部的零(请记住,SVG垂直轴在向下方向上为正)。最后,我们将 scale 声明与方法 .nice() 链接起来,这将确保域以“nice”舍入值结束,而不是注释数据集中的实际最大值。
示例 5.6 声明垂直刻度(堆叠条.js)const maxUpperBoundary = d3.max(annotatedData[annotatedData.length - 1], d
➥ => d[1]);
const yScale = d3.scaleLinear()
.domain([0, maxUpperBoundary])
.range([innerHeight, 0])
.nice();
我们现在已准备好附加条形图。为此,我们遍历带注释的数据,并逐个附加序列,如清单 5.7 中所述。我们从数据绑定模式开始,为系列数组中的每个项目或年份附加一个矩形元素(每种音乐格式都有一个系列)。请注意我们如何将与当前系列相关的类名应用于矩形并将其用作选择器。如果我们简单地使用“rect”元素作为选择器,每次执行循环时,先前创建的矩形都将被删除并替换为新矩形。
然后,我们通过调用带刻度的带宽属性来设置矩形的 x 属性,通过将当前年份传递给 xScale 来设置它们的宽度属性。y 属性对应于矩形左上角的垂直位置,由前面声明的垂直刻度返回,我们将矩形的上边界 (d[1] ) 传递到该刻度。
同样,矩形的高度是其上边界和下边界位置之间的差异。这里有一点问题。因为 SVG 垂直轴在向下方向上是正的,所以 yScale(d[0]) 返回的值高于 yScale(d[1])。我们需要从前者中减去后者,以避免为 y 属性提供负值,这会引发错误。
最后,我们通过将当前音乐格式传递给色阶来设置 fill 属性,该色阶可在每个系列的 key 属性下访问,如前面图 5.13 所示。
清单 5.7 追加堆积条(堆积条.js)annotatedData.forEach(serie => { #A
innerChart
.selectAll(`.bar-${serie.key}`) #B
.data(serie) #B
.join("rect") #B
.attr("class", d => `bar-${serie.key}`) #B
.attr("x", d => xScale(d.data.year)) #C
.attr("y", d => yScale(d[1])) #C
.attr("width", xScale.bandwidth()) #C
.attr("height", d => yScale(d[0]) - yScale(d[1])) #C
.attr("fill", colorScale(serie.key)); #C
});
如果保存项目,您将看到条形之间没有水平空间。我们可以通过回到 xScale 的声明来解决这个问题,并将其 paddingInner() 访问器函数设置为值 20%,就像我们在第 3 章中所做的那样。
示例 5.8 在条形之间添加填充(刻度.js)xScale
.domain(data.map(d => d.year))
.range([0, innerWidth])
.paddingInner(0.2);
为了完成我们的堆积条形图,我们需要添加轴。在清单 5.9 中,我们首先使用方法 d3.axisBottom() 声明一个底部轴,并将 xScale 作为引用传递。
我们将轴声明与方法链接起来, .tickValues() ,它允许我们陈述我们希望在图表上看到的确切刻度和标签。否则,D3 将每年提供一对刻度和标签,看起来会很局促且难以阅读。方法.tickValues()将值数组作为参数。我们使用方法 d3.range() 生成这个数组,并声明我们想要从 1975 年到 2020 年的每个整数,步长为 5。
我们还使用方法 .tickSizeOuter() 隐藏底部轴两端的刻度,我们向其传递值为零。方法tickValues()和tickSizeOuter()都可以在d3轴模块(https://github.com/d3/d3-axis)中找到,而d3.range()是d3-array模块(https://github.com/d3/d3-array)的一部分。
最后,我们使用 call() 方法将底部轴附加到图表中,在转换为底部的组中,并对左轴执行相同的操作。
清单 5.9 追加轴(堆叠条.js)const bottomAxis = d3.axisBottom(xScale) #A
.tickValues(d3.range(1975, 2020, 5)) #A
.tickSizeOuter(0); #A
innerChart #B
.append("g") #B
.attr("transform", `translate(0, ${innerHeight})`) #B
.call(bottomAxis); #B
const leftAxis = d3.axisLeft(yScale); #C
innerChart #C
.append("g") #C
.call(leftAxis); #C
如果保存项目并在浏览器中查看它,您可能会发现轴标签有点太小。此外,如第 4 章所述,D3 将字体族“sans-serif”应用于包含轴元素的 SVG 组,这意味着项目的字体系列不会被继承。从 CSS 文件可视化中.css ,我们可以使用选择器 .tick 文本定位轴标签并修改其样式属性。在下面的代码片段中,我们更改了它们的字体系列、字体大小和字体粗细属性。
.tick text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 500;
}
完成后,堆积条形图将类似于图 5.15 中的条形图,但看起来还不像图 5.2 中的条形图或托管项目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 中的条形图。我们一会儿就到那里。
图5.15 第一版堆积条形图
5.2.3 绘制流线图在上一小节中,我们使用堆栈布局函数生成一个带注释的数据集,从中绘制堆叠条形图的矩形。现在,我们将应用类似的策略来绘制流图。尽管流图看起来比堆积条形图更复杂,但它们很容易在 D3 中创建。主要区别在于,对于流图,我们使用带注释的数据集来追加区域,而为堆叠条形图附加矩形。
在本小节中,我们将使用函数 drawStreamGraph() ,您可以在文件流图中找到它.js 。此函数已包含将 SVG 容器附加到 div 的代码,ID 为 “streamgraph”,以及内部图表的组容器。这与我们在第4章中使用的策略相同,与保证金惯例并行。
在示例 5.10 中,我们初始化堆栈生成器并调用它来获取带注释的数据。我们还声明了一个线性刻度来计算垂直边界的位置。这与我们用于堆积条形图的代码完全相同。现在,不要担心我们正在复制代码。我们将在下一小节中回到它。
示例 5.10 声明堆栈生成器和垂直轴(流图.js)const stackGenerator = d3.stack() #A
.keys(formatsInfo.map(f => f.id)); #A
const annotatedData = stackGenerator(data); #A
const maxUpperBoundary = d3.max(annotatedData[annotatedData.length - 1], d
➥ => d[1]);
const yScale = d3.scaleLinear() #B
.domain([0, maxUpperBoundary]) #B
.range([innerHeight, 0]) #B
.nice(); #B
为了绘制堆叠区域,我们需要一个区域生成器函数,该函数将负责计算用于绘制序列的每个路径元素的 d 属性。如第4章所述,面积生成器至少使用三个访问器函数,在我们的例子中,一个用于检索每个数据点的水平位置,一个用于堆叠区域的下边界,另一个用于它们的上边界。图 5.16 说明了面积生成器如何应用于堆叠区域。
图 5.16 面积生成器 d3.area() 与三个或更多访问器函数组合在一起。当与流图的堆栈布局结合使用时,它使用每个数据点的下限和上限(y0 和 y1)来计算区域的 d 属性。
在下面的代码片段中,我们初始化了区域生成器 d3.area() 。首先,我们使用 x() 访问器函数来计算每个数据点的水平位置。由于 xScale 是波段刻度,因此它返回相关年份的每个波段开头的位置,该位置可在每个数据点的数据对象中的注释数据集中访问 ( d.data.year)。如果我们希望数据点与下面堆叠条形图的条形中心水平对齐,我们需要将数据点向右平移,宽度为条形宽度的一半,我们可以用带宽()属性计算带刻度。
然后,我们使用 y0() 和 y(1) 访问器函数来确定数据点沿每个序列的下边界和上边界的垂直位置。这个位置是用 yScale 计算的,之前声明了,我们将边界的值传递给边界值,可以通过边界数据中的数组索引访问:d[0] 表示下边界,d[1] 表示上限边界。
最后,如果我们想沿每个边界插值数据点以获得曲线而不是直线,我们使用 curve() 访问器函数。这里我们选择了曲线插值函数d3.curveCatmullRom。如前所述,曲线插值会修改数据的表示,必须谨慎选择。有关讨论和演示,请参阅第 4.2.2 节。
const areaGenerator = d3.area()
.x(d => xScale(d.data.year) xScale.bandwidth()/2)
.y0(d => yScale(d[0]))
.y1(d => yScale(d[1]))
.curve(d3.curveCatmullRom);
现在,我们已准备好绘制堆叠区域!首先,我们使用数据绑定模式为注释数据集中的每个序列生成一个 SVG 路径元素。我们调用面积生成器函数来获取每个路径的 d 属性,以及它们的填充属性的色阶。
请注意我们如何在 SVG 组中附加路径以保持标记井井有条且易于检查。这也将有助于在以后保持区域和垂直网格的适当并置。
innerChart
.append("g")
.attr("class", "areas-container")
.selectAll("path")
.data(annotatedData)
.join("path")
.attr("d", areaGenerator)
.attr("fill", d => colorScale(d.key));
在本节中,我们要做的最后一件事是向流图添加轴和标签。我们开始声明轴生成器 d3.axisLeft() 并将 yScale 作为引用传递。然后,我们使用 .call() 方法将轴元素附加到 SVG 组中。
const leftAxis = d3.axisLeft(yScale);
innerChart
.append("g")
.call(leftAxis);
将轴展开到网格中
我们可能会省略 x 轴,因为流图与下面的堆叠条形图水平对齐,并且此图表具有相同的 x 轴。但我们将利用这个机会讨论如何扩展轴上的刻度以在图表后面创建网格。
首先,我们需要记住,SVG 元素是按照它们在 SVG 容器中出现的顺序绘制的。因此,如果我们希望网格出现在流线图后面,我们需要先绘制它。这就是为什么以下代码片段应位于追加流图路径的代码片段之前的原因。
到目前为止,生成底部轴的代码与用于堆叠条形图的代码相同,包括 tickValues() 和 tickSizeOuter() 方法的使用。
const bottomAxis = d3.axisBottom(xScale)
.tickValues(d3.range(1975, 2020, 5))
.tickSizeOuter(0);
innerChart
.append("g")
.attr("class", "x-axis-streamgraph")
.attr("transform", `translate(0, ${innerHeight})`)
.call(bottomAxis);
要将即时报价转换为网格,我们所要做的就是使用 tickSize() 方法扩展它们的长度。通过这种方法,我们给即时报价一个对应于内部图表高度的长度,乘以 -1 使它们向上增长。请注意,我们还可以首先避免平移轴,并将此长度设置为正值,以使刻度线从上到下的方向增长。每当需要水平网格时,此方法也可以应用于左轴或右轴。
const bottomAxis = d3.axisBottom(xScale)
.tickValues(d3.range(1975, 2020, 5))
.tickSizeOuter(0)
.tickSize(innerHeight * -1);
最后,我们可以选择隐藏轴底部的水平线和年份标签,方法是将它们的不透明度定为零。为此,我们使用之前赋予 x 轴容器的类名( x-axis-streamgraph ),并将其用作 CSS 文件可视化中的选择器.css .正如您在下面的代码片段中看到的,通过“ .x-axis-streamgraph path”访问的水平线的不透明度是用stroke-opacity属性管理的,而我们需要使用填充不透明度来隐藏年份标签(“ .x-axis-streamgraph文本”)。我们还可以使用 D3 style() 方法来处理流图内的不透明度.js .
.x-axis-streamgraph path {
stroke-opacity: 0;
}
.x-axis-streamgraph text {
fill-opacity: 0;
}
处理复杂的 svg 文本布局
最后,我们将在左侧轴上方添加一个标签,以指示此轴所代表的内容。如图 5.2 所示,或者在托管项目 (https://d3js-in-action-third-edition.github.io/visualizing-40-years-of-music-industry-sales/) 上,流图的标签分为两行,第一行带有文本“总收入(百万美元)”,第二行提到“根据通货膨胀进行调整”。
我们将使用 SVG 文本构建此标签。关于 SVG 文本,需要了解的一件事是它的行为不像 HTML 文本。例如,如果我们在 HTML 元素中添加文本,文本将根据水平可用空间自动换行或重排。SVG 文本不会这样做,每个文本元素的位置需要单独处理。
要操作 SVG 文本中的潜台词,我们可以使用 tspan 元素。将文本分解为多个 tspan,允许使用其 x、y、dx 和 dy 属性分别调整其样式和位置,前两个用于参考 SVG 容器的坐标系,后两个用于参考前一个文本元素。
在上述所有定义中,请务必记住,文本基线由其文本锚点属性水平控制,垂直由其主基线属性控制。
为了创建我们的标签,我们可以使用位于 SVG 文本中的三个 tspan 元素,如图 5.17 所示。如果文本元素的主基线属性设置为 hang ,则文本将显示在 SVG 容器原点的正下方和右侧。使用 dx 和 dy,我们可以根据图 5.17 分别将第二个和第三个跨度移动到它们的正确位置。
图 5.17 tspan 元素允许分别操作副词项的样式和位置。我们使用属性 dx 和 dy 来设置相对于前一个文本元素的位置。
在下面的代码片段中,我们将该策略付诸行动。首先,我们将一个文本元素附加到我们的 SVG 容器中,并将其主基线属性设置为值 hang ,这意味着文本及其子文本的基线将位于它们的正上方。
我们将文本选择保存到常量 leftAxisLabel 中,并重复使用它将三个 tspan 元素附加到文本容器中。我们将第一个tspan的文本设置为“总收入”,第二个tspan设置为“(百万美元)”,第三个tspan设置为“经通货膨胀调整”。
默认情况下,tspan 元素一个接一个地显示在同一水平线上。保存您的项目并查看标签进行确认。
const leftAxisLabel = svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)");
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation");
要将第二个 tspan 稍微向右移动,我们可以设置其 dx 属性并为其指定值 5。要将第三个 tspan 移动到第一个和第二个 tspan 下方,我们可以使用 y 或 dy 属性并为其指定值“20”。在这种特殊情况下,这两个属性将具有相同的效果。最后,如果我们希望第三个 tspan 的左侧与 SVG 容器的左边框对齐,最好使用 x 属性并将其设置为零。
const leftAxisLabel = svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)")
.attr("dx", 5);
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation")
.attr("x", 0)
.attr("dy", 20);
通常,tspan 元素用于将不同的样式应用于文本的一部分。例如,我们可以降低第二个和第三个 tspan 元素的不透明度,使它们呈灰色,并减小第三个 tspan 的字体大小,因为与标签的其余部分相比,它传达了次要信息。
const leftAxisLabel = svg
.append("text")
.attr("dominant-baseline", "hanging");
leftAxisLabel
.append("tspan")
.text("Total revenue");
leftAxisLabel
.append("tspan")
.text("(million USD)")
.attr("dx", 5)
.attr("fill-opacity", 0.7);
leftAxisLabel
.append("tspan")
.text("Adjusted for inflation")
.attr("x", 0)
.attr("dy", 20)
.attr("fill-opacity", 0.7)
.style("font-size", "14px");
我们的流图的第一次迭代现在已经完成,如图 5.18 所示。当此类图表的垂直基线位于零时,我们通常将其命名为堆积面积图,而流线图的面积往往位于中心基线周围。在下一小节中,我们将讨论如何更改图表的基线。但在我们到达那里之前,观察堆叠条形图和堆叠面积图在这一点上的相似之处很有趣。
图 5.18 我们流线图的第一次迭代,也可以命名为堆积面积图。
5.2.4 堆栈顺序和堆栈偏移属性通过控制序列的堆叠顺序以及它们在零基线周围的垂直定位方式,我们可以将堆积条形图和堆积面积图更进一步。此级别的控制是通过 order() 和 offset() 访问器函数实现的,这两个函数都应用于堆栈布局生成器。
让我们首先看一下 order() 访问器函数,它控制形状垂直堆叠的顺序。D3 有六个内置订单,可以作为参数传递,如图 5.19 所示。
d3.stackOrderNone 是默认顺序,这意味着如果未设置 order() 访问器函数,则应用该顺序。它按与 keys 数组中列出的顺序相同的顺序堆叠对应于每个系列的形状,从下到上。d3.stackOrderReverse颠倒了这个顺序,从底部的最后一个键开始,到顶部的第一个键结束。
d3.stackOrderAscending 计算每个序列的总和。总和最小的序列位于底部,其他序列按升序堆叠。同样,d3.stackOrder降序将总和最大的序列放在底部,并按降序堆叠序列。
最后两个订单计算每个序列达到其最大值的指数。d3.stackOrderAppearance 按序列达到峰值的顺序堆叠序列,这对于可读性非常有用,尤其是对于基线为零的堆栈。另一方面,d3.stackOrderInsideOut 将峰值最早的序列定位在图表的中间,将最新峰值的序列放在外面。此顺序非常适合形状围绕中心基线分布的流线图。
图 5.19 D3 允许使用 order() 访问器函数控制形状堆叠的顺序。在这里,我们看到堆积区域的示例,但相同的原则适用于堆积条形图。
堆栈布局的另一个访问器函数称为 offset() ,控制图表零基线的位置以及形状在其周围的分布方式。D3 有五个内置偏移量,如图 5.20 所示。
d3.stackOffsetNone 将所有形状定位在零基线上方。它是默认偏移量。
以下三个偏移分布基线上方和下方的形状。d3.stackOffsetDiverging 将正值定位在基线上方,负值定位在基线下方。此偏移最适合堆积条形图。d3.stackOffsetSilhouette 将基线移动到图表的中心。d3.stackOffsetWiggle的作用类似,但优化了基线的位置,以最小化摆动或序列的交替上下移动。这三个偏移需要调整垂直刻度的域以适应基线的位置。
最后,d3.stackOffsetExpand 规范化 0 到 1 之间的数据值,使每个索引的总和为 100%。归一化值时,垂直刻度的域也在 0 和 1 之间变化。
图 5.20 D3 允许使用 offset() 访问器函数控制形状相对于基线的位置。在这里,我们看到堆积区域和堆积条形的示例。在创建堆叠布局时,我们通常会组合顺序和偏移量以达到所需的结果。虽然对于我们何时应该使用顺序或偏移量没有严格的规定,但目标应始终是提高可视化的可读性和/或将注意力集中在我们想要强调的故事上。
对于本章的项目,我们将使用 order() 和 offset() 访问器函数将堆积面积图转换为具有中心基线和堆积条形图以表示相对值(介于 0 和 100% 之间)的流图。
在我们开始之前需要注意的一件事是,order() 和 offset() 访问器函数可以显着更改注释数据集中携带的值。例如,通过将堆积面积图转换为流图,所表示的销售价值将不再在 24 到 000,12 之间变化,而是在 -000,12 和 000,3 之间变化。同样,如果我们使用 d0.stackOffsetExpand 来规范堆叠条形图显示的销售额,则注释数据将包含在 1 到 <> 之间。在设置垂直刻度的域时,必须考虑这些不同的值。
考虑不同 offset() 访问器函数带来的域变化的一种简单方法是确保我们始终计算注释数据集中的最小值和最大值,并相应地设置域。
在示例 5.11 中,我们首先声明两个空数组,一个存储每个序列的最小值,另一个存储最大值。然后我们遍历带注释的数据集,使用 d3.min() 和 d3.max() 找到每个序列的最小值和最大值,并将它们推送到相应的数组中。最后,我们从每个数组中提取最小值和最大值,并使用它们来设置域。
此策略可应用于流图和堆积条形图。对于堆积条形图,您可能希望从比例声明中删除 nice() 方法,以仅显示介于 0 和 1 之间的值。
示例 5.11 计算 yScale 域的最小值和最大值(堆积条形图.js 流图.js)const minLowerBoundaries = []; #A
const maxUpperBoundaries = []; #A
annotatedData.forEach(series => { #B
minLowerBoundaries.push(d3.min(series, d => d[0])); #B
maxUpperBoundaries.push(d3.max(series, d => d[1])); #B
}); #B
const minDomain = d3.min(minLowerBoundaries); #C
const maxDomain = d3.max(maxUpperBoundaries); #C
const yScale = d3.scaleLinear()
.domain([minDomain, maxDomain]) #D
.range([innerHeight, 0])
.nice();
完成此修改后,您可以自由测试偏移值的任何顺序,并且 yScale 的域将自动调整。
现在,要将堆叠面积图转换为流图,我们所要做的就是将 order() 和 offset() 访问器函数链接到之前声明的堆栈生成器。在这里,我们使用订单 d3.stackOrderInsideOut 与偏移量 d3.stackOffsetSilhouette 结合使用。我们鼓励您测试一些组合,以了解它们如何影响数据表示。
const stackGenerator = d3.stack()
.keys(formatsInfo.map(f => f.id))
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetSilhouette);
可视化提示
流线图在美学上令人愉悦,它们肯定会吸引注意力。但它们也更难阅读。当您想要概述现象随时间推移的演变时,流图是一个很好的选择。但是,如果您希望读者能够精确地测量和比较值,堆叠条形图或成对条形图是更好的选择。工具提示还可以帮助提高流图的可读性。我们将在第 7 章中构建一个。
同样,我们通过将其偏移量设置为 d3.stackOffsetExpand 来修改堆积条形图,这将规范化 0 到 1 之间的销售值。我们还将顺序设置为 d3.stackOrderDescending,以强调 CD 格式在 2000 年左右如何主导市场。再次尝试一些组合,看看它如何改变图表传达的故事焦点。
const stackGenerator = d3.stack()
.keys(formatsInfo.map(f => f.id))
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetExpand);
5.3 向项目添加图例
在最后一节中,我们将讨论如何使用传统的 HTML 元素轻松构建图例,并将通过在堆叠条形图下方放置颜色图例来将其付诸实践。图例是数据可视化的重要组成部分,可帮助读者解释他们所看到的内容。
通常,图例涉及文本,我们知道 SVG 文本并不总是便于操作。如果您查看我们将在图 5.21 中构建的颜色图例,您会发现它由一系列彩色方块和标签组成,与堆叠条形图水平居中。使用 SVG 元素构建此图例将涉及计算每个矩形和文本元素的确切位置。这是可能的,但有一种更简单的方法。
图 5.21 我们将在本节中构建的颜色图例,位于堆积条形图下方。
D3 不仅用于控制 SVG 元素。它可以创建和操作任何 DOM 元素。这意味着我们可以使用传统的HTML元素构建图例,并使用CSS来定位它们。有很多方法可以继续,但这样的图例要求结构化为 HTML 无序列表 ( <ul></ul> )。带有标签的每个颜色组合都可以存储在 <li></li> 元素中,其中一个 <span></span> 元素保存颜色,另一个元素包含标签,如以下示例所示。
<ul>
<li>
<span> color 1 </span>
<span> label 1 </span>
</li>
<li>
<span> color 2 </span>
<span> label 2 </span>
</li>
...
</ul>
要使用 D3 构建此 HTML 结构,我们转到文件图例.js并开始在函数 addLegend() 中工作。在下面的代码片段中,我们选择带有一类 legend-container 的 div,该类已存在于索引中.html .我们将一个 ul 元素附加到这个 div 中,并给它一类颜色图例。
然后,我们使用数据绑定模式为 formatInfo 数组中包含的每种格式附加一个 li 元素,该数组在共享常量中可用.js .我们将此选择保存到一个常量中 命名 图例项 .
我们调用 legendItems 选择并将 span 元素附加到其中,并根据相关的音乐格式设置跨度的背景颜色属性。为此,我们可以直接从 格式信息 或调用色标。最后,我们附加另一个 span 元素并将其文本设置为当前格式的标签键。
const legendItems = d3.select(".legend-container")
.append("ul") #A
.attr("class", "color-legend") #A
.selectAll(".color-legend-item") #A
.data(formatsInfo) #A
.join("li") #A
.attr("class", "color-legend-item");
legendItems #B
.append("span") #B
.attr("class", "color-legend-item-color") #B
.style("background-color", d => d.color); #B
legendItems #C
.append("span") #C
.attr("class", "color-legend-item-label") #C
.text(d => d.label); #C
如果您应用的类名与上一代码段中使用的类名相同,则图例应自动如图 5.21 所示。这是因为以下样式已在 base 中设置.css .请注意我们如何使用 CSS flexbox 属性 (https://css-tricks.com/snippets/css/a-guide-to-flexbox/) 来处理图例的布局。我们不会花时间解释这个样式片段,因为您可能熟悉CSS,这不是本书的重点。这里的主要要点是,有时传统的HTML元素和CSS样式比SVG更容易操作,我们可以使用D3来绑定数据和操作任何DOM元素。
.color-legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 0;
padding-left: 0;
}
.color-legend-item {
margin: 5px 12px;
font-size: 1.4rem;
}
.color-legend span {
display: inline-block;
}
.color-legend-item-color {
position: relative;
top: 2px;
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 3px;
}
您现在知道如何使用 D3 布局,如饼图和堆栈布局。在第7章中,我们将把这个项目变成一个交互式可视化。如果您接下来想这样做,请随时直接去那里。
5.4 小结Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved