D3.js实战教程:4 绘制直线、曲线和圆弧


本章涵盖

  • 向图表添加轴并应用边距约定
  • 使用线条生成器功能绘制折线图
  • 插值数据点以将直线转换为曲线
  • 使用面积生成器绘制面积
  • 使用电弧发生器创建电弧

您已经熟悉我们使用和组合用于制作数据可视化的常见 SVG 形状:线条、矩形和圆形。您甚至已经使用矩形从头开始创建了条形图。但是我们可以用原始形状画出的东西太多了。为了创建更复杂的可视化,我们通常转向 SVG 路径。正如我们在第 1 章中所讨论的,SVG 路径是所有 SVG 元素中最灵活的,几乎可以采用任何形式。我们在 D3 项目中广泛使用它们,最简单的例子是在折线图中绘制线条和曲线或在圆环图中绘制弧线。

SVG 路径的形状由其 d 属性确定。此属性由指示路径的起点和终点、用于更改方向的曲线类型以及路径是打开还是闭合的命令组成。路径的 d 属性可能很快变得又长又复杂。大多数时候,我们不想自己创作它。这就是 D3 的形状生成器功能的用武之地!

在本章中,我们将构建如图 4.1 所示的项目:温度演变的折线图和一组弧线,可视化 2021 年纽约市降水天数的百分比。您可以在 https://d3js-in-action-third-edition.github.io/new-york-city-weather-2021/ 在线找到此项目。基础数据来自地下天气(www.wunderground.com/)。

图 4.1 我们将在本章中构建的项目:2021 年纽约市温度演变的折线图和一组显示降水天数百分比的弧线图。


我们将使用 D3 的形状生成器函数创建这两个可视化效果。但在开始之前,我们将讨论 D3 的边距约定以及如何向图表添加轴。

4.1 创建轴

开发数据可视化通常需要提前规划如何使用 SVG 容器中的可用空间。首先开始玩很酷的东西是非常诱人的,也就是可视化的核心,但相信我们。一点点的准备可以为您节省大量的执行时间。所有编程任务都是如此,在一般生活中也是如此!在此规划阶段,我们不仅要考虑图表本身,还要考虑使图表可读的互补元素,例如轴、标签和图例。

在本节中,我们将介绍边距约定,这是一种便于为这些不同元素分配空间的方法。然后,我们将讨论如何向可视化添加轴以及组成 D3 轴的多个 SVG 元素。我们将这些概念应用于图 4.1 所示的折线图。

在我们开始之前,请转到第 4 章的代码文件。您可以从本书的 Github 存储库下载它们(如果您还没有 (https://github.com/d3js-in-action-third-edition/code-files)。在名为 chapter_04 的文件夹中,代码文件按节进行组织。要开始本章的练习,请在代码编辑器中打开 4.1-Margin_convention_and_axes/start 文件夹并启动本地 Web 服务器。如果您需要有关设置本地开发环境的帮助,请参阅附录 A。

您可以在位于本章代码文件根目录下的自述文件中找到有关项目文件夹结构的更多详细信息。

警告

使用本章的代码文件时,在代码编辑器中仅打开一个开始文件夹或一个结束文件夹。如果一次打开章节的所有文件并使用 Live Server 扩展为项目提供服务,则数据文件的路径将无法按预期工作。

我们将开始在文件折线图中工作.js并使用方法 d3.csv() 加载每周温度数据集。

d3.csv("../data/weekly_temperature.csv");

在第 3 章中,我们解释了 D3 在加载表格数据集时执行的类型转换会影响值的类型。例如,原始数据集中的数字变成字符串,我们需要将它们变回数字以方便操作它们。我们已经看到 d3.csv() 的回调函数 ,我们可以逐行访问数据,是执行此类转换的好地方。在这里,我们将介绍一个小技巧。我们可以调用方法 d3.autoType ,而不是手动转换数字。此函数检测常见的数据类型,如日期和数字,并将它们转换为相应的 JavaScript 类型。

d3.csv("../data/weekly_temperature.csv", d3.autoType);

请注意,数据类型可能不明确,并且 d3.autoType 有时会选择错误的类型。因此,在数据数组完全加载后仔细检查数据数组非常重要。在下面的代码片段中,我们使用 JavaScript Promise 访问加载的数据集,并将其登录到控制台,以确认日期被格式化为 JavaScript 日期,温度被格式化为 数字。您可以在图 4.2 中看到结果。

d3.csv("../data/weekly_temperature.csv", d3.autoType).then(data => {
  console.log("temperature data", data);
});

图 4.2 由于 d3.autoType 方法,日期被格式化为 JavaScript 日期,温度被格式化为数字。


我们使用 JavaScript Promise 来访问数据集,因为加载数据是一个异步过程(如果您需要复习有关使用 D3 加载和访问数据的信息,请参阅第 3 章)。但是现在我们知道我们的数据集已完全加载并正确格式化,我们可以开始构建图表了。

文件折线图.js已经包含一个名为 drawLineChart() ,我们将在其中创建折线图。在 JavaScript Promise 的回调函数中,调用函数 drawLineChart() 并将数据集作为参数传递。

d3.tsv("../data/weekly_temperature.csv", d3.autoType).then(data => {
  console.log("temperature data", data);
  drawLineChart(data);
});

我们现在准备讨论保证金惯例并将其应用于我们的图表!

4.1.1 边距约定

D3 边距约定旨在以系统和可重用的方式为轴、标签和图例保留图表周围的空间。该约定使用四个边距:图表的上方、右侧、下方和左侧,如图 4.3 所示。通过说明这些边距,我们可以知道图表核心剩余区域的位置和大小,我们称之为内部图表。

图 4.3 D3 边距约定设置图表顶部、右侧、底部和左侧的边距值。


边距值在边距对象中声明,该对象由上边距、右边距、下边距和左边距组成。让我们为折线图创建边距对象。在函数 drawLineChart() 中,声明一个名为 margin 的常量。如以下代码片段所示,为上边距、右边距、下边距和左边距分别指定 40、170、25 和 40px 的值。

const drawLineChart = (partialData) => {
  const margin = {top: 40, right: 170, bottom: 25, left: 40};
};

事先确切知道轴和标签需要多少空间通常是不可能的。我们从一个有根据的猜测开始,如果需要,稍后会进行调整。例如,查看图 4.1 中的折线图或托管项目 (https://d3js-in-action-third-edition.github.io/new-york-city-weather-2021/)。您将看到可视化效果右侧显示的标签相对较长,因此右边距为 170px。另一方面,轴的标签不占用太多空间;因此,剩余的边距可以小得多。

声明边距对象后,我们就可以开始考虑 SVG 容器的大小了。知道了 SVG 容器的大小和边距,我们最终可以计算出两个新常量,分别名为 innerWidth 和 innerHeight ,它们代表内部图表的宽度和高度。这些尺寸如图4.4所示。

图 4.4 知道 SVG 容器的尺寸和边距,我们可以计算内部图表的宽度和高度。


内部图表的宽度对应于 SVG 容器的宽度减去左侧和右侧的边距。如果 SVG 容器的宽度为 1000 像素,每边的边距分别为 170 和 40 像素,则内部图表仍保留 790 像素。同样,如果 SVG 容器的高度为 500px,我们通过从总高度中减去顶部和底部边距来计算内部图表的高度,因此为 435px。通过使常量 innerWidth 和 innerHeight 与边距成正比,我们确保如果我们以后需要更改边距,它们会自动调整。

const margin = {top: 40, right: 170, bottom: 25, left: 40};
const width = 1000;
const height = 500;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

现在让我们附加折线图的 SVG 容器。仍然在函数 drawLineChart() 中工作,将一个 SVG 元素附加到 div 中,其 id 为 折线图,该元素已经存在于文件索引中.html并使用宽度和高度常量设置其 viewBox 属性。您还可以临时将边框应用于 SVG 元素,以帮助您查看正在工作的区域。如果您需要复习如何将元素附加到 DOM 或设置其属性和样式,请参阅第 2 章。

const svg = d3.select("#line-chart")
  .append("svg")
  .attr("viewBox", `0, 0, ${width}, ${height}`);

我们之前已经声明了边距,这些边距将决定为内部图表保留的区域。知道 SVG 容器的坐标系从其左上角开始,内部图表的每个元素都必须向保留区域移动。我们可以将内部图表包装在 SVG 组中并仅对该组应用平移,而不是将此置换应用于每个元素。如图 4.5 所示,此策略为内部图表创建了一个新的坐标系。

图 4.5 应用于将包含内部图表的 SVG 组的平移,为内部图表中包含的元素创建新的坐标系。


为了将此策略付诸实施,我们将一个组附加到 SVG 容器。然后,我们根据左边距和上边距对组应用翻译。最后,我们将 SVG 组保存到名为 innerChart 的常量中,稍后我们将使用该常量构建折线图。

const innerChart = svg
  .append("g")
  .attr("transform", `translate(${margin.left}, ${margin.top})`);

保证金约定和此处介绍的策略的主要优点是,一旦实施,我们就不再需要考虑它了。我们可以继续创建轴和图表,同时知道为标签、图例和其他补充信息保留了一个区域。

4.1.2 生成轴

建立边距约定后,我们准备向图表添加轴。轴是数据可视化的重要组成部分。它们可作为查看者理解所代表的数字和类别的参考。

如果您查看图 4.1 中的折线图或托管项目 (https://d3js-in-action-third-edition.github.io/new-york-city-weather-2021/),您将看到两个轴。水平轴,也称为 x 轴,显示每个月的位置。垂直轴或 y 轴用作以华氏度为单位的温度的参考。

在 D3 中,我们使用 axis() 组件生成器创建轴。此生成器将比例作为输入,并返回组成轴的 SVG 元素作为输出。如果您还记得我们在第 3 章中关于比例的讨论,您就会知道它们将数据值映射到屏幕上。例如,对于我们的折线图,刻度将为我们计算数据集中每个日期的水平位置或其相关温度的垂直位置。

声明秤

创建轴的第一步实际上是声明其比例。首先,我们需要一个水平定位日期的刻度。这正是 D3 的时间尺度 d3.scaleTime() 的作用(有关选择 D3 尺度的帮助,请参阅附录 B)。时间尺度是第3章讨论的第一类尺度的一部分。它接受连续输入并返回连续输出。时间尺度的行为与第3章中使用的线性尺度非常相似,唯一的区别是它操纵与时间相关的数据并计算它们在空间中的位置。

让我们声明我们的时间刻度并将其命名为 xScale,因为它将负责沿 x 轴定位元素。我们规模的范围从数据集中的第一个日期延伸到最后一个日期。在下面的代码片段中,我们使用 d3.min() 和 d3.max() 来查找这些值。

刻度所涵盖的范围随着内部图表中可用的水平空间而扩展(见图4.5)。在内部图表的坐标系中,这意味着范围从零扩展到之前计算的 innerWidth。如果您需要复习声明 D3 刻度的域和范围,请参阅第 3 章。

const firstDate = d3.min(data, d => d.date);
const lastDate = d3.max(data, d => d.date);
const xScale = d3.scaleTime()
  .domain([firstDate, lastDate])
  .range([0, innerWidth]);

沿y轴分布的温度也需要第一个系列的刻度,具有连续的输入和输出。线性刻度在这里将是完美的,因为我们希望温度和折线图上的垂直位置是线性比例的。

在下面的代码片段中,我们声明了我们的温标并将其命名为 yScale,因为它将负责沿 y 轴定位元素。在这里,我们希望我们的 y 轴从零开始,因此我们将零作为域的第一个值传递。尽管数据集中的最低温度约为 26°F,但从零开始 y 轴通常是一个好主意,在我们的例子中,这将使我们能够正确看到温度的演变。但就像生活中的大多数事情一样,这不是一个硬性规则,这个图表没有正确或错误的答案,特别是因为华氏度的零不是绝对的零。

我们将数据集中的最高温度作为域的第二个值传递。我们通过使用函数 d3.max() 查询数据集中的列max_temp_F来找到此值。

我们的刻度范围随着内部图表的高度而扩展。由于垂直值是在 SVG 坐标系中从上到下计算的,因此范围从 innerHeight(内图左下角的位置)开始,到零(对应于其左上角的位置)结束。

const maxTemp = d3.max(data, d => d.max_temp_F);
const yScale = d3.scaleLinear()
  .domain([0, maxTemp])
  .range([innerHeight, 0]);

追加轴

初始化刻度后,我们就可以附加轴了。D3 有四个轴生成器:axisTop()、axisRight()、axisBottom() 和 axisLeft() ,它们分别创建顶部、右侧、底部和左侧轴的组件。它们都是 d3 轴模块 (https://github.com/d3/d3-axis) 的一部分。

我们提到轴生成器函数将刻度作为输入。例如,要创建折线图的底部轴,我们调用生成器 axisBottom() 并将 xScale 作为参数传递,因为此刻度负责沿底部轴分布数据。我们将生成器保存在名为 底轴 .

const bottomAxis = d3.axisBottom(xScale);

轴生成器是构造组成轴的元素的函数。为了使这些元素出现在屏幕上,我们需要使用 call() 方法从 D3 选择中调用轴生成器。在下面的代码片段中,请注意我们如何在调用轴生成器之前使用 innerChart 选择并将组元素附加到其中。该组的类名为 axis-x,这将帮助我们稍后定位和设置轴的样式。

const bottomAxis = d3.axisBottom(xScale);
innerChart
  .append("g")
    .attr("class", "axis-x")
    .call(bottomAxis);

图 4.6 默认情况下,D3 轴在所选内容的原点生成,此处为内部图表的左上角。我们需要应用翻译将它们移动到所需的位置。


在浏览器中查看生成的轴。默认情况下,D3 轴显示在所选内容的原点,在本例中为内部图表区域的左上角,如图 4.6 所示。我们可以通过对包含轴的 SVG 组应用平移来将轴移动到图表底部。请记住,应用于组的转换由其所有子级继承。在下面的代码片段中,我们将包含轴元素的组向下平移一个对应于内部图表高度的值。

const bottomAxis = d3.axisBottom(xScale);
innerChart
  .append("g")
    .attr("class", "axis-x")
    .attr("transform", `translate(0, ${innerHeight})`)
    .call(bottomAxis);

我们要更改的另一件事是轴标签的格式。默认情况下,D3 调整轴上的时间表示形式,根据域显示小时、天、月或年标签。但是这种默认格式并不总是提供我们正在寻找的标签。幸运的是,D3 提供了多种方法来更改标签的格式。

首先,我们注意到 x 轴有 3 月至 <> 月的标签,这很好,但没有 <> 月的标签。根据您居住的时区,第一个日期可能不完全是 <> 月 <> 日的午夜,这使 D<> 无法将其识别为我们第一个月的开始。由于我们的数据集不是动态的,因此对 firstDate 变量进行硬编码是一个合理的解决方案。为此,我们将使用 JavaScript Date() 构造函数。

在下面的代码片段中,firstDate 成为一个新的 Date() 对象。在括号之间,我们首先声明年份 ( 2021 年 )、月份 ( 00,因为月份索引为零索引)、日 ( 01 ),并可选择在它后面跟小时、分钟和秒 ( 0, 0, 0 )。

const firstDate = new Date(2021, 00, 01, 0, 0, 0);
const lastDate = d3.max(data, d => d.date);
const xScale = d3.scaleTime()
  .domain([firstDate, lastDate])
  .range([0, innerWidth]);

如果保存项目,你将看到我们现在在 1 月 2021日的位置有一个标签。但是标签只给了我们 01 年,这并没有错,因为 Fri Jan 2021 00 00:00:2021 对应于 <> 年的开始,但我们更愿意有一个月份标签。

图 4.7 默认情况下,D3 调整轴标签上的时间表示。在我们的例子中,它表示 1 月 2021日作为 <> 年的开始。这没有错,但对于可读性来说并不理想。


我们可以使用方法 axis.tickFormat() 更改轴标签的格式,该方法在 d3 轴模块 (https://github.com/d3/d3-axis) 中可用。刻度是您在轴上看到的短垂直线。它们通常(但不一定)附有勾号标签。

假设我们希望刻度标签是缩写的月份名称。在 D3 中,我们可以使用方法 d3.timeFormat() 格式化与时间相关的值,来自模块 d3-time-format (https://github.com/d3/d3-time-format)。此方法接受格式作为参数,例如,%b 表示月份名称的缩写。您可以在模块中查看可用格式的完整列表。

在下面的代码片段中,我们将 tickFormat() 方法链接到之前声明的底部轴,并将时间格式作为参数传递。

const bottomAxis = d3.axisBottom(xScale)
  .tickFormat(d3.timeFormat("%b"));

图 4.8 使用每个月缩写名称格式化的底部轴标签。


我们的标签现在格式正确!它们标记每个月的开始,这还不错,但我们可以通过在各自的刻度之间居中月标签来提高可读性,以建议每个月从一个刻度延伸到下一个刻度。

要更改刻度标签的位置,我们首先需要选择它们。打开浏览器的检查器,仔细查看 D3 为轴生成的 SVG 元素。首先,我们有一个带有类域的路径元素,该元素在范围(或域的表示)上绘制一条水平线。此路径包括两个外部刻度,即形状两端的短垂直线,如图 4.9 所示。轴的刻度和标签由线条和文本元素组成,组织成具有刻度类的 SVG 组。这些 SVG 组沿轴平移以设置其行和文本元素的位置。轴生成器创建的元素的类型和类是 D3 公共 API 的一部分。您可以使用它们来自定义轴外观。

图 4.9 组成轴的 SVG 元素


考虑到这种结构,我们可以使用选择器选择x轴的所有标签 “.axis-x 文本” ,这意味着我们使用类轴-x抓取组中的每个文本元素。然后我们执行一些调整。首先,我们使用文本元素的 y 属性将文本元素向下移动 10px。这种增加的垂直空白将提高可读性。我们还将他们的字体系列设置为Roboto,这是我们已经在项目中使用的字体。默认情况下,D3 将轴的字体系列设置为无衬线,防止标签继承项目的字体系列。最后,我们将它们的字体大小增加到 14px。

出于关注点分离的目的,最后两个样式调整最好从CSS文件中处理。但在这里,我们使用 D3 来简化指令。

d3.selectAll(".axis-x text")
  .attr("y", "10px")
  .style("font-family", "Roboto, sans-serif")
  .style("font-size", "14px");

为了使月份标签在其相应的刻度之间居中,我们将使用 x 属性。由于每个月都有不同的长度(在 28 到 31 天之间),我们需要为每个标签找到该月第一天和下个月第一天之间的中位数位置。请注意,D3 已在 g.axis-x 上将文本锚点属性设置为“中间”。

我们知道 D3 附加到每个标签的数据对应于该月的第一天。在下面的代码片段中,我们通过将 JavaScript 方法 getMonth() 应用于当前月份或附加到标签的值来查找下个月。此方法返回一个介于 0 和 11 之间的数字,0 表示 11 月,<> 表示 <> 月。然后,我们可以通过将年份、下个月和每月的第一天传递给 Date() 对象来创建新的 JavaScript 日期。

最后,我们使用 xScale 计算月初和下个月开始之间的中位数距离。完成后,您的轴应如图 4.10 所示。

d3.selectAll(".axis-x text")
  .attr("x", d => {
    const currentMonth = d;
    const nextMonth = new Date(2021, currentMonth.getMonth() + 1, 1);
    return (xScale(nextMonth) - xScale(currentMonth)) / 2;
  })
  .attr("y", "10px")
  .style("font-family", "Roboto, sans-serif")
  .style("font-size", "14px");

图 4.10 格式化的 x 轴,月份标签在各自的刻度之间居中。


那是很多操纵!但希望它能让您了解我们可以自定义 D3 轴的不同方法。

我们现在将添加 y 轴,其步骤将更加简单。我们使用轴生成器 d3.axisLeft() ,因为我们想将 y 轴定位在图表的左侧。我们将 yScale 作为参数传递,并将轴保存在名为 leftAxis 的常量中。

const leftAxis = d3.axisLeft(yScale);

再一次,我们希望将轴附加到内部图表。我们将一个组附加到内部图表选择中,给它一个 axis-y 类并调用 leftAxis 。

const leftAxis = d3.axisLeft(yScale);
innerChart
  .append("g")
  .attr("class", "axis-y")
  .call(leftAxis);

如果保存项目并在浏览器中查看,则会看到 y 轴已正确定位。我们所要做的就是更改标签的字体并增加它们的大小。在下面的代码片段中,我们使用类轴-y 选择组内的所有文本元素。我们使用它们的 x 属性将它们稍微向左移动以获得更好的可读性,并设置它们的字体系列和字体大小属性。

d3.selectAll(".axis-y text")
  .attr("x", "-5px")
  .style("font-family", "Roboto, sans-serif")
  .style("font-size", "14px");

您可能已经注意到,我们必须重复代码来设置轴标签的字体系列和字体大小属性。在学习环境中,这没什么大不了的,但我们通常会尽量避免在专业项目中出现这种重复。前面提到的更好的解决方案是从CSS文件控制这些样式。另一种可能是使用组合选择器应用它们,如下所示。

d3.selectAll(".axis-x text, .axis-y text")
  .style("font-family", "Roboto, sans-serif")
  .style("font-size", "14px");

图 4.11 完成的 x 轴和 y 轴。


添加轴标签

我们已经完成了我们的轴,但我们仍然应该做一件事来帮助读者理解我们的图表。x 轴上的标签是不言自明的,但 y 轴上的标签不是。我们知道它们在 0 到 90 之间变化,但我们不知道它们代表什么。

我们可以通过向轴添加标签来解决此问题。在 D3 项目中,标签只是文本元素,所以我们所要做的就是将文本元素附加到 SVG 容器中。我们将其内容设置为“温度(°F)”,并将其垂直位置设置为SVG容器原点下方20px。就是这样!您的项目现在应如图 4.12 所示。在下一节中,我们将绘制折线图。

svg
  .append("text")
  .text("Temperature (°F)")
  .attr("y", 20);

图 4.12 完成的轴和标签。


4.2 绘制折线图

现在,我们已准备好构建最常见的数据可视化之一:折线图。折线图由连接数据点的线或插入这些数据点的曲线组成。它们通常用于显示现象随时间推移的演变。在 D3 中,这些直线和曲线是使用 SVG 路径元素构建的,这些元素的形状由其 d 属性确定。在第 1 章中,我们讨论了 d 属性是如何由一系列命令组成的,这些命令指示如何绘制形状。我们还说过,它很快就会变得复杂。值得庆幸的是,d3 形状模块 (https://github.com/d3/d3-shape) 提供了为我们计算 d 属性的线和曲线生成器函数,简化了折线图的创建。

在本节中,我们将绘制一条线/曲线,显示 2021 年纽约市平均温度的演变,就像您在托管项目 (https://d3js-in-action-third-edition.github.io/new-york-city-weather-2021/) 或图 4.1 中看到的那样。但首先,让我们在屏幕上显示每个数据点。虽然这一步对于绘制折线图不是必需的,但它将帮助我们了解 D3 的线生成器函数的工作原理。

在函数 drawLineChart() 中工作,我们使用数据绑定模式为数据集weekly_temperature.csv中的每一行创建一个圆圈。我们将这些圆圈附加到 innerChart 选择中,并给它们一个 4px 的半径。然后我们使用x和y尺度计算它们的位置属性(cx和cy)。

如果你还记得我们从第 3 章开始关于数据绑定的讨论,你就知道我们可以使用访问器函数访问绑定到每个圆圈的数据。在下面的代码片段中,d 公开了附加到每个圆的基准面。这些数据是一个 JavaScript 对象,我们可以使用点符号访问日期或平均温度。如果您需要查看此概念,请参阅第 3.3.1 节。

请注意我们如何声明一个名为“茄子”的单独颜色常量,并使用它来设置圆圈的填充属性。在这个项目中,我们将重复使用相同的颜色几次,因此将其放在常量中会很方便。随意使用您喜欢的任何颜色!

const aubergine = "#75485E";
innerChart
  .selectAll("circle") #A
  .data(data)          #A
  .join("circle")      #A
    .attr("r", 4)
    .attr("cx", d => xScale(d.date))       #B
    .attr("cy", d => yScale(d.avg_temp_F)) #B
    .attr("fill", aubergine);

保存您的项目并查看浏览器中的圆圈。它们应位于 29 到 80°F 之间,并形成圆顶状形状,如图 4.13 所示。

图 4.13 平均温度随时间演变的数据点。


您现在可以绘制散点图

在这个阶段要指出的一件很酷的事情是,即使没有注意到它,你现在也知道如何绘制散点图!散点图只是一个图表,显示沿 x 轴和 y 轴定位的数据点集合,并可视化两个或多个变量之间的关系。

您知道如何绘制轴,并且知道如何根据其相关数据在屏幕上定位数据点,因此您可以完全构建散点图!这就是D3的酷之处。您不必学习如何创建特定的图表。相反,您可以通过生成和组装构建基块来构建可视化效果。对于散点图,这些构建基块可以像两个轴和一组圆一样简单。在第 7 章中,我们将构建一个散点图,其中圆的面积根据变量而变化。

散点图示例


4.2.1 使用线路生成器

现在我们清楚地看到了每个数据点的位置,引入 D3 的线生成器会更容易。行生成器 d3.line() 是一个函数,它将每个数据点的水平和垂直位置作为输入,并返回线或折线的 d 属性,作为输出传递通过这些数据点。我们通常用两个访问函数 x() 和 y() 链接线生成器,分别将数据点的水平和垂直位置作为参数,如图 4.14 所示。

图 4.14 行生成器 d3.line() 与两个访问器函数 x() 和 y() 结合使用,它们分别将每个数据点的水平和垂直位置作为参数。


让我们为折线图声明一个线生成器函数。我们首先调用方法 d3.line() 并使用 x() 和 y() 访问器函数进行链接。x() 访问器函数将每个数据点的水平位置作为参数。如果我们像到目前为止所做的那样遍历数据,我们可以使用参数 d 来访问每个基准面(数据集的每一行)。数据点的水平位置对应于它们表示的日期,并使用 xScale() 计算。同样,数据点的垂直位置与当天的平均温度成正比,并由 yScale() 返回。我们将行生成器函数存储在名为 lineGenerator 的常量中,以便稍后可以调用它。

const lineGenerator = d3.line()
  .x(d => xScale(d.date)) #A
  .y(d => yScale(d.avg_temp_F)); #B

然后,我们将一个路径元素附加到内部图表,并通过调用线生成器并将数据集作为参数传递来设置其 d 属性。

默认情况下,SVG 路径具有黑色填充。如果我们只想看到一条线,我们需要将填充属性设置为 none 并将笔触属性设置为我们选择的颜色;这里,颜色存储在茄子常数中。此笔画将成为我们的折线图,如图 4.15 所示。

innerChart
  .append("path")
    .attr("d", lineGenerator(data)) #A
    .attr("fill", "none")
    .attr("stroke", aubergine);

图 4.15 使用线生成器创建并穿过每个数据点的 SVG 路径,生成折线图。


4.2.2 将数据点插值到曲线中

在像我们的折线图这样的情况下,离散数据点覆盖了整个数据范围,用简单的线条表示数据点是一个很好的解决方案。但有时,我们需要在点之间插值数据,为此 D3 提供了各种生成曲线的插值函数。

曲线生成器用作 d3.line() 的访问函数。要将上一节中声明的线生成器转换为曲线生成器,我们只需链接 curve() 访问器函数并传递 D3 的一个插值器。在下面的代码片段中,我们使用插值器 d3.curveCatmullRom ,它产生一个三次样条曲线(通过每个数据点并使用三阶多项式函数计算的平滑灵活的形状)。结果如图4.16所示。

const curveGenerator = d3.line()
  .x(d => xScale(d.year))
  .y(d => yScale(d.electoral_democracies))
  .curve(d3.curveCatmullRom);

图 4.16 使用Catmull-Roll样条进行曲线插值的折线图。


什么是最好的插值?

插值会修改数据表示,不同的插值函数会创建不同的可视化效果。数据可以通过各种方式可视化,从编程的角度来看,所有这些都是正确的。但是,我们有责任确保我们可视化的信息反映了实际现象。

由于数据可视化处理统计原理的可视化表示,因此它受到滥用统计数据的所有危险的影响。线条的插值特别容易被误用,因为它将一条看起来笨拙的线条变成了一条平滑的“自然”线条。

在图 4.17 中,您可以看到使用不同曲线插值跟踪的相同折线图,并了解它们如何影响视觉表示。选择适当的插值函数在很大程度上取决于您正在使用的数据。在我们的例子中,d3.curveBasis低估了温度的突然变化,而d3.curveBundle旨在拉直曲线并减少其变化,这对于我们的数据来说是不够的。如果我们没有在图表上绘制数据点,我们就不知道曲线不能准确地表示它们。这就是为什么仔细选择和测试曲线插值函数很重要的原因。

另一方面,函数 d3.curveMonotoneX 和 d3.curveCatmullRom 创建紧随数据点的曲线,类似于原始折线图。d3.curveStep 还可以在上下文适当时提供对数据的有趣解释。图 4.17 中所示的曲线插值列表并不详尽,其中一些插值器还接受影响最终曲线形状的参数。有关所有可用选项,请参阅 d3 形状模块 (https://github.com/d3/d3-shape)。

图 4.17 不同的曲线插值及其如何修改数据的表示。


您现在知道如何使用 D3 绘制折线图了!回顾一下,我们首先需要初始化一个线生成器函数并设置其 x() 和 y() 访问器函数。这些将负责计算每个数据点的水平和垂直位置。然后,我们可以通过链接 curve() 访问器函数并选择插值来选择将直线转换为曲线。最后,我们将一个 SVG 路径元素附加到我们的图表中,并通过调用线条生成器并将数据作为参数传递来设置其 d 属性。在第 7 章中,我们将通过工具提示使此折线图具有交互性。如果您想立即学习该章节,请随时直接转到该章节!

图 4.18 创建折线图的步骤


4.3 绘制区域

在本节中,我们将在折线图后面添加一个区域,以显示每个日期的最低和最高温度之间的范围。在 D3 中绘制区域的过程与用于绘制线条的过程非常相似。像线条一样,区域是使用 SVG 路径元素创建的,D3 为我们提供了一个方便的区域生成器函数, d3.area() ,用于计算该路径的 d 属性。

在开始之前需要注意的一件事是,我们希望显示折线图后面的区域。由于元素在屏幕上的绘制顺序与它们追加在 SVG 父项中的顺序相同,因此应在创建折线图的代码之前添加用于绘制区域的代码。

4.3.1 使用面积生成器

让我们首先声明一个区域生成器函数,并将其存储在名为 areaGenerator 的常量中。正如您在以下代码片段中观察到的那样,区域生成器至少需要三个访问器函数。第一个 x() 负责计算数据点的水平位置,与线生成器完全相同。但是现在,我们不仅有一组数据点,而是两组:一个沿着区域的下边缘,另一个在其上边缘,因此访问器函数 y0() 和 y1() 。请注意,在我们的例子中,区域下边缘和上边缘的数据点共享相同的水平位置。

const areaGenerator = d3.area()
  .x(d => xScale(d.date))
  .y0(d => yScale(d.min_temp_F))
  .y1(d => yScale(d.max_temp_F));

图 4.19 可能有助于可视化区域的下限和上限,以及面积生成器如何计算与该区域相关的数据。

图 4.19 面积生成器 d3.area() 与三个或更多访问器函数结合使用。为了绘制最低和最高温度之间的面积,我们使用 x()、y0() 和 y1()。第一个计算每个数据点的水平位置,第二个计算数据点在下边界上的垂直位置,这里是最低温度,第三个是数据点在上边缘的垂直位置,这里是最高温度。


正如我们对折线图所做的那样,通过将 curve() 访问器函数链接到面积生成器,将区域的边界插值为曲线。这里我们也使用相同的曲线插值器函数, d3.curveCatmullRom 。

const areaGenerator = d3.area()
  .x(d => xScale(d.date))
  .y0(d => yScale(d.min_temp_F))
  .y1(d => yScale(d.max_temp_F))
  .curve(d3.curveCatmullRom);

一旦面积生成器准备就绪,我们需要做的就是将 SVG 路径元素附加到内部图表中。为了设置其 d 属性,我们调用区域生成器并将数据集作为参数传递。其余的纯粹与美学有关。我们将填充属性设置为之前声明的茄子色常数,并将填充不透明度设置为 20%,以确保区域和折线图之间的对比度足够。请注意,茄子常数的声明需要在我们使用它来设置区域的填充之前进行。

innerChart
  .append("path")
    .attr("d", areaGenerator(data))
    .attr("fill", aubergine)
    .attr("fill-opacity", 0.2);

图4.20 平均温度的折线图,结合显示最低温度和最高温度之间变化的区域。


如您所见,绘制区域的过程与绘制线条的过程非常相似。主要区别在于,一条线只有一组数据点,在这些数据点之间绘制了这条线,而区域是两条边之间的区域,每条边都有一组数据点。这就是为什么线发生器只需要两个访问器函数 x() 和 y() ,而面积生成器至少需要三个,在我们的例子中是 x() 、y0() 和 y1()。

图 4.21 创建区域的步骤


4.3.2 使用标签增强可读性

我们现在有一张 2021 年纽约市平均温度的折线图,以及一个显示最低和最高温度之间变化的区域。它看起来已经相当不错了,但我们需要确保看到这张图表的人很容易理解线条和面积的含义。标签是一个很好的工具!

在 D3 中,标签只是我们放置在可视化效果上的 SVG 文本元素。在这里,我们将创建三个标签,一个用于我们将放置在折线图末尾的平均温度,一个用于放置在该区域下方的最低温度,另一个用于放置在该区域上方的最高温度。

让我们从折线图的标签开始。我们首先将 SVG 文本元素附加到内部图表,并使用 text() 方法将其内容设置为“平均温度”。然后我们计算它的位置,由属性 x 和 y 控制。

我们希望标签位于折线图的末尾或紧靠其最后一个数据点之后。我们可以通过将之前声明刻度时计算的 lastDate 常量传递给 xScale() 来获取该值。我们还添加了 10px 的额外填充。

对于垂直位置,我们还没有一个常数来为我们提供最后一个温度值。尽管如此,我们仍然可以使用 data[data.length - 1] 找到数据集中的最后一行,并使用点符号来访问平均温度。我们将这个值传递给 yScale() 并获取标签的垂直位置。

最后,我们重用颜色常数茄子作为文本的颜色,由其填充属性控制。

innerChart
  .append("text")
    .text("Average temperature")
    .attr("x", xScale(lastDate) + 10)
    .attr("y", yScale(data[data.length - 1].avg_temp_F))
    .attr("fill", aubergine);

如果保存项目并在浏览器中查看,则会发现标签的底部与折线图上最后一个数据点的中心垂直对齐。默认情况下,SVG 文本的基线位于文本底部,如图 4.22 所示。我们可以使用主导基线属性来更改此设置。在下面的代码片段中,我们给此属性一个值 中间 ,以将基线移动到文本的垂直中心。

图 4.22 SVG 文本的 y 属性设置其基线的垂直位置,默认情况下位于文本底部。我们可以使用主导基线属性来更改它。如果我们给此属性值“middle”,则文本的基线将移动到其垂直中间,而值“hanging”会将基线移动到文本的顶部。


innerChart
  .append("text")
    .text("Average temperature")
    .attr("x", xScale(lastDate) + 10)
    .attr("y", yScale(data[data.length - 1].avg_temp_F))
    .attr("dominant-baseline", "middle")
    .attr("fill", aubergine);

然后,我们将为该区域的下边界添加一个标签,该标签表示最低温度的演变。策略非常相似。我们首先附加一个 SVG 文本元素,并为其提供“最低温度”的内容。

对于它的位置,我们选择了最后一个向下的突起,它对应于倒数第三个数据点。我们将这些数据点的值传递给我们的秤以找到它的位置,并将标签向下移动 20px,向右移动 13px。这些数字是通过移动标签找到的,直到我们找到一个看起来合适的位置。浏览器的检查器工具是测试此类微小调整的好地方。请注意,我们已将标签的主要基线设置为 挂起 。如图 4.22 所示,这意味着 y 属性控制文本顶部的位置。

最后,在代码段中,您将看到我们在标签中添加了一条线,在该区域的向下突起和标签之间跟踪,以阐明标签代表的内容。您可以在图 4.23 中看到它的外观。同样,我们使用刻度来计算直线的 x1、y1、x2 和 y2 属性,这些属性控制其起点和终点的位置。

innerChart
  .append("text")
    .text("Minimum temperature")
    .attr("x", xScale(data[data.length - 3].date) + 13)
    .attr("y", yScale(data[data.length - 3].min_temp_F) + 20)
    .attr("alignment-baseline", "hanging")
    .attr("fill", aubergine);
innerChart
  .append("line")
    .attr("x1", xScale(data[data.length - 3].date))
    .attr("y1", yScale(data[data.length - 3].min_temp_F) + 3)
    .attr("x2", xScale(data[data.length - 3].date) + 10)
    .attr("y2", yScale(data[data.length - 3].min_temp_F) + 20)
    .attr("stroke", aubergine)
    .attr("stroke-width", 2);

我们使用非常相似的过程为该区域的上边界附加一个标签,该标签表示最高温度的演变。我们选择将此标签放置在与倒数第四个数据点相对应的向上突起附近。同样,我们在标签和突起之间画了一条线。完成后,折线图就完成了!

innerChart
  .append("text")
    .text("Maximum temperature")
    .attr("x", xScale(data[data.length - 4].date) + 13)
    .attr("y", yScale(data[data.length - 4].max_temp_F) - 20)
    .attr("fill", aubergine);
innerChart
  .append("line")
    .attr("x1", xScale(data[data.length - 4].date))
    .attr("y1", yScale(data[data.length - 4].max_temp_F) - 3)
    .attr("x2", xScale(data[data.length - 4].date) + 10)
    .attr("y2", yScale(data[data.length - 4].max_temp_F) - 20)
    .attr("stroke", aubergine)
    .attr("stroke-width", 2);

图 4.23 2021年纽约市温度演变的完整折线图。


4.4 绘制圆弧

在最后一节中,我们将讨论如何使用 D3 绘制弧线。弧形是数据可视化中的常见形状。它们用于饼图、旭日图和南丁格尔图,以可视化金额与总数的关系,我们经常在自定义径向可视化中使用它们。

像直线和面积一样,弧是用 SVG 路径绘制的,而且,正如你现在可能已经猜到的那样,D3 提供了一个方便的弧发生器函数,可以为我们计算弧路径的 d 属性。

在详细讨论电弧发生器之前,让我们准备我们的项目。在这里,我们将绘制构成径向图的弧线,您可以在图 4.1 中的“有降水的日子”或托管项目 (https://d3js-in-action-third-edition.github.io/new-york-city-weather-2021/) 中看到。蓝色弧线表示 2021 年纽约市有降水的天数百分比 (35%),而灰色弧线表示其余天数。

首先,打开文件弧.js .这就是我们将在本章其余部分工作的地方。像往常一样,我们需要加载一个数据集,在本例中为 daily_precipitations.csv ,它包含在数据文件夹中。如果您查看 CSV 文件,您会发现它只包含两列:日期列列出了 2021 年的每一天,而total_precip_in列则提供了每天的总降水量(以英寸为单位)。

在下面的代码片段中,我们使用 d3.csv() 获取数据集,使用 d3.autoType 正确格式化日期和数字,并使用 Promise 将其链接,我们将数据记录到控制台中。我们不会在这里讨论如何使用 d3.csv() 的细节。有关更多说明,请参阅第 3 章,有关 d4.autoType 的讨论,请参阅本章的第 1.3 节。

d3.csv("./data/daily_precipitations.csv", d3.autoType).then(data => {
  console.log("precipitations data", data);
});

如果您在控制台中查看数据,您会发现日期和数字的格式都正确。伟大!我们可以获取格式化的数据集并将其传递给函数 drawArc() ,它已经存在于 arcs 中.js .

d3.csv("./data/daily_precipitations.csv", d3.autoType).then(data => {
  console.log("precipitations data", data);
  drawArc(data);
});

在 drawArc() 中,我们现在可以附加一个新的 SVG 容器。正如您在以下代码片段中看到的,我们为 SVG 容器提供了 300px 的宽度和高度,并将其附加到 div 中,其中包含索引中已经存在的 arc id.html 。我们使用第 1 章中解释的策略使 SVG 响应:将 viewBox 属性的最后两个值设置为其宽度和高度,并完全省略宽度和高度属性。这样,SVG 容器将适应其父容器的大小,同时保留其纵横比。请注意,我们将 SVG 容器选择保存在名为 svg 的常量中。

const pieChartWidth = 300;
const pieChartHeight = 300;
const svg = d3.select("#arc")
  .append("svg")
  .attr("viewBox", [0, 0, pieChartWidth, pieChartHeight]);

4.4.1 极坐标系

如第 4.1 节所述,我们将图表包装在 SVG 组中,并将该组转换为所需位置。不过,这次的策略有点不同。我们不需要为轴或标签保留空间,因此我们可以省略边距约定。但是,与迄今为止构建的所有可视化相反,弧位于极坐标系中,而不是笛卡尔坐标系中,后者的行为略有不同。

如图 4.24 所示,SVG 容器的坐标系是笛卡尔坐标系。它使用两个垂直维度 x 和 y 来描述 2D 空间中的位置。我们在第 1 章中讨论过,SVG 元素的坐标系有点特殊,因为它的原点位于 SVG 容器的左上角,使 y 维在从上到下的方向上为正。

2D 极坐标系还使用两个维度:半径和角度。半径是原点与空间中点之间的距离,而角度是从 12 点钟方向沿顺时针方向计算的。这种描述空间位置的方法在处理圆弧时特别有用。

图 4.24 笛卡尔坐标的尺寸彼此垂直,而极坐标系统使用半径和角度尺寸来描述空间中的位置。


由于元素位于极坐标系中的原点周围,因此我们可以说我们将要构建的弧可视化的原点位于 SVG 容器的中心,如图 4.25 所示。

图 4.25 通过将弧包装成 SVG 组并将该组转换为 SVG 容器的中心,我们简化了一组弧的创建。当我们向组追加弧时,它们的位置将自动相对于图表的中心,这对应于其极坐标系的原点。


在下一个代码片段中,我们选择SVG容器并在其中附加一个组,我们将该组转换为SVG容器的中心,并将其保存在常量innerChart中。

const innerChart = svg
  .append("g")
    .attr("transform", `translate(${pieChartWidth/2}, 
       ➥ ${pieChartHeight/2})`);

在创建弧线之前,我们需要做最后一件事:计算图表上有降水的日子所采用的角度。使用 D3 创建饼图或圆环图时,我们通常使用饼图布局生成器处理此类计算,我们将在下一章中介绍。但是由于我们在这里只画两个弧线,所以数学很容易。

首先,我们可以使用数据集的 length 属性知道 2021 年的总天数,即 365。然后,我们通过过滤数据集来查找有降水的天数,以仅保留降水量大于零的天数,即 126 天。最后,我们将降水的天数除以总天数(得到 35%),将降水天数转换为百分比。

const numberOfDays = data.length;
const numberOfDaysWithPrecipitations = data.filter(d => 
  ➥ d.total_precip_in > 0).length;
const percentageDaysWithPrecipitations =   
  ➥ Math.round(numberOfDaysWithPrecipitations / numberOfDays * 100);

然后,我们可以通过将这个数字乘以 360 度(一个完整圆的度数)来计算对应于降水天数的角度,得到 126 度。我们从度开始,因为它往往更直观,但我们还需要将此值转换为弧度。为此,我们将降水天数百分比(以度为单位)所覆盖的角度乘以数字 pi (3.1416),然后将其除以 180,得到大约 2.2 弧度的角度,我们将其保存在常数angleDaysWithPrecipitations_rad中。

我们执行此转换是因为我们将在一会儿使用的电弧发生器期望角度以弧度而不是度为单位。作为处理角度的经验法则,JavaScript 通常希望它们以弧度为单位,而 CSS 使用度数。

const angleDaysWithPrecipitations_deg = percentageDaysWithPrecipitations * 
  ➥ 360 / 100;
const angleDaysWithPrecipitations_rad = angleDaysWithPrecipitations_deg * 
  ➥ Math.PI / 180;

4.4.2 使用电弧发生器

我们终于到了有趣的部分,生成弧线!首先,我们需要声明一个电弧发生器,就像我们对线和区域所做的那样。弧发生器 d3.arc() 是模块 d3-shape (https://github.com/d3/d3-shape) 的一部分,在我们的例子中,需要两个主要的访问器函数:弧的内半径和外半径,分别由 innerRadius() 和 outerRadius() 处理,并给定值为 80 和 120px。请注意,如果内半径为零,我们会得到一个类似于饼图中的弧线,并从原点开始。

const arcGenerator = d3.arc()
  .innerRadius(80)
  .outerRadius(120);

我们可以通过使用访问器函数在弧形之间添加填充来个性化我们的弧线 padAngle() ,它接受以弧度为单位的角度。这里我们使用 0.02 弧度,对应于略多于 1 度。我们也可以用 角半径() ,它接受一个以像素为单位的值。此访问器函数与 CSS 边框半径属性具有类似的效果。

const arcGenerator = d3.arc()
  .innerRadius(80)
  .outerRadius(120)
  .padAngle(0.02)
  .cornerRadius(6);

图 4.26 电弧发生器使用多个访问器函数来计算电弧的 d 属性。在这里,我们在生成器声明期间设置其内半径、外半径、填充角度和角半径。我们将在将路径元素附加到图表时传递每个弧的开始和结束角度。


此时,您可能想知道为什么我们不使用处理弧线覆盖的角度的访问器函数。在我们的例子中,由于我们已经手动计算了角度,因此当我们附加路径时,将这些值传递给电弧发生器会更简单。但我们将在下一章中看到,情况并非总是如此。

因此,让我们附加第一个弧线,即显示降水天数的弧线。在下面的代码片段中,我们首先将一个 path 元素附加到内部图表选择中。然后,我们通过调用最后一个代码段中声明的 arc 生成器来设置其 d 属性。

观察我们如何将开始和结束角度作为对象传递给生成器。起始角度的值为零,对应于 12 点钟位置,而结束角度的值是之前计算的降水天数所覆盖的角度。最后,我们将弧线的填充设置为颜色 #6EB7C2,青蓝色。

innerChart
  .append("path")
    .attr("d", () => {
      return arcGenerator({
        startAngle: 0,
        endAngle: angleDaysWithPrecipitations_rad
      });
    })
    .attr("fill", "#6EB7C2");

我们以类似的方式附加第二个弧线。这一次,弧从前一个弧线结束的地方开始,到圆圈完成时结束,对应于弧度中的角度 2*Pi。我们给弧线一个 #DCE2E2,一种更接近灰色的颜色,以表明这些日子没有降水。

innerChart
  .append("path")
    .attr("d", () => {
      return arcGenerator({
        startAngle: angleDaysWithPrecipitations_rad,
        endAngle: 2 * Math.PI
      });
    })
    .attr("fill", "#DCE2E2");

保存项目后,弧应如图 4.27 所示。我们鼓励您使用传递给生成器的访问器函数的值(如半径或角半径),以了解它们如何修改弧的外观。

图 4.27 弧线显示有降水天数和无降水天数之间的比率。


如您所见,绘制圆弧的过程类似于绘制线条和区域的过程。主要区别在于弧在空间中的位置是用极坐标而不是笛卡尔来处理的,这反映在弧发生器的访问器函数中。

图 4.28 绘制弧线的步骤。


4.4.3 计算弧的质心

饼图和圆环图最近在数据可视化社区中得到了很多负面报道,主要是因为我们意识到人眼不太擅长估计弧线所代表的比率。但是,这些图表并不总是一个糟糕的选择,尤其是当它们包含少量类别时。但我们绝对可以通过标签帮助他们提高可读性,这就是我们在这里要做的!

在表示有降水天数的弧线上,我们将添加标签“35%”,即之前计算的有降水的天数百分比。放置此标签的好地方是弧的质心,也称为其质心。此值可由电弧发生器提供。

在下面的代码片段中,我们在前面初始化的弧发生器函数上调用方法。这一次,我们将它与 startAngle() 和 endAngle() 访问器函数链接起来,分别将它们传递代表有降水的日子的弧的开始角和结束角的值。最后,我们链接方法centroid(),它将计算弧的中点。

const centroid = arcGenerator
  .startAngle(0)
  .endAngle(angleDaysWithPrecipitations_rad)
  .centroid();

将质心记录到控制台中。您将看到它由两个值的数组组成:质心的水平和垂直位置,在我们的例子中是 [89, -45] ,从内部图表的原点计算得出。

在下一个代码段中,我们通过向内部图表追加文本元素来创建标签。为了使标签包含“%”符号,我们使用方法 d3.format(“.0%”) ,后跟括号中的值。此方法便于以特定方式(如货币、百分比和指数)格式化数字,或为这些数字添加特定后缀,如“M”表示百万或“μ”表示微型。您可以在模块 d3 格式 (https://github.com/d3/d3-format) 中找到所有可用格式的详细列表。

然后,我们使用质心数组中返回的第一个和第二个值设置 x 和 y 属性。请注意我们如何设置文本锚点和主要基线属性,以确保标签在水平和垂直方向上以 x 和 y 属性为中心。

最后,我们给标签一个白色和500的字体粗细,以提高其易读性。保存后,带有标签的弧应如图 4.29 所示。

innerChart
  .append("text")
    .text(d => d3.format(".0%")(percentageDaysWithPrecipitations/100))
    .attr("x", centroid[0])
    .attr("y", centroid[1])
    .attr("text-anchor", "middle")
    .attr("dominant-baseline", "middle")
    .attr("fill", "white")
    .style("font-weight", 500);

图 4.29 带有标签的已完成弧。


您现在知道如何使用 D3 绘制线条、面积和弧线了!在下一章中,我们将使用布局生成器将这些形状提升到另一个层次。

4.5 小结

  • D3 边距约定的作用是以系统和可重用的方式为轴、标签和图例保留图表周围的空间。
  • 我们通过声明一个包含上边距、右边距、下边距和左边距值的边距对象来实现这一点。
  • 一个有用的策略是将构成图表本身的元素包装到 SVG 组中,并根据边距将此组放置在 SVG 容器中。这将为图表元素创建一个新的原点,并促进其实现。
  • D3 有四个轴生成器:axisTop()、axisRight()、axisBottom() 和 axisLeft() ,它们分别创建顶部、右侧、底部和左侧轴的组件。
  • 这些轴生成器将刻度作为输入,并返回组成轴的 SVG 元素作为输出(沿轴的一条线以及多组刻度和标签)。
  • 通过将 call() 方法链接到选择并将轴作为参数传递,我们将轴附加到图表。
  • 折线图是最常见的图表之一,可用于显示现象随时间推移的演变。我们绘制带有连接数据点的线条或曲线的折线图。为了绘制折线图,我们首先使用 d3.line() 方法初始化一个线生成器。线生成器有两个访问器函数,x() 和 y(),它们计算每个数据点的水平和垂直位置。我们可以使用 curve() 访问器函数将折线图转换为曲线。D3提供多种曲线插值函数,这些函数会影响数据表示,必须仔细选择。为了使折线图显示在屏幕上,我们将路径元素附加到选择中,并通过调用线生成器并将数据集作为属性传递来设置其 d 属性。
  • 面积是两个边界之间的区域,使用 D3 绘制区域类似于绘制一条线。为了绘制一个区域,我们首先用方法 d3.area() 声明一个区域生成器。此方法至少需要三个访问器函数来计算每个数据点沿区域边缘的位置,例如 x() 、y0() 和 y1() 或 x0()、x1() 和 y()。与直线一样,D3 提供了可与 curve() 访问器函数一起应用的插值函数。为了使区域出现在屏幕上,我们将路径元素附加到选择中,并通过调用区域生成器并将数据集作为属性传递来设置其 d 属性。
  • 标签对于帮助读者理解我们的数据可视化特别有用。在 D3 中,标签只是我们需要在 SVG 容器中定位的文本元素。SVG 文本的位置由其 x 和 y 属性控制。y 属性设置文本基线的位置,默认情况下,基线位于其底部。我们使用属性移动 SVG 文本的基线 主导基线 .值中间将基线移动到文本的垂直中间,而值挂起 将基线移动到顶部。
  • 使用弧的可视化通常使用极坐标系进行描述。此坐标系使用半径、原点与点之间的距离以及角度来描述空间中的位置。
  • 弧是使用 SVG 路径元素创建的,其中 d 属性是使用弧发生器计算的。D3 的弧发生器 d3.arc() 具有访问函数,用于定义弧的起始和结束角度( startAngle() 和 endAngle() ),以及它的内半径和外半径( innerRadius() 和 outerRadius() )。我们还可以使用访问器函数来圆弧的角( cornerRadius() ) 或在弧之间添加填充 ( padAngle() )。电弧发生器期望角度以弧度表示。
  • 弧的质心可以用 centroid() 方法计算。此访问器函数链接到电弧发生器,返回一个包含质心水平和垂直位置的数组。
举报
评论 0