内部图表的宽度对应于 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 使用每个月缩写名称格式化的底部轴标签。