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);});
我们现在准备谈论担保金老例并将其运用于我们的图表!
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");
我们以类似的办法附加第二个弧线。这一次,弧从前一个弧线结束的地方开始,到圆圈完成时结束,对应于弧度中的角度 2Pi。我们给弧线一个 #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 绘制线条、面积和弧线了!
不才一章中,我们将利用布局天生器将这些形状提升到另一个层次。