当前内容所在位置(可进入专栏查看其他译好的章节内容)
- 第一部分 D3.js 基础知识
- 第一章 D3.js 简介(已完结)
- 1.1 何为 D3.js?
- 1.2 D3 生态系统——入门须知
- 1.3 数据可视化最佳实践(上)
- 1.3 数据可视化最佳实践(下)
- 1.4 本章小结
- 第二章 DOM 的操作方法(已完结)
- 2.1 第一个 D3 可视化图表
- 2.2 环境准备
- 2.3 用 D3 选中页面元素
- 2.4 向选择集添加元素
- 2.5 用 D3 设置与修改元素属性
- 2.6 用 D3 设置与修改元素样式
- 2.7 本章小结
- 第三章 数据的处理(已完结)
- 3.1 理解数据
- 3.2 准备数据
- 3.3 将数据绑定到 DOM 元素
- 3.3.1 利用数据给 DOM 属性动态赋值
- 3.4 让数据适应屏幕
- 3.4.1 比例尺简介(上篇)
- 3.4.2 线性比例尺(中篇)
- 3.4.2.1 基于 Mocha 测试 D3 线性比例尺(DIY 实战)
- 3.4.3 分段比例尺(下篇)
- 3.4.3.1 使用 Observable 在线绘制 D3 条形图(DIY 实战)
- 3.5 加注图表标签(上篇)
- 3.5.1 人物专访:Krisztina Szűcs(下篇)
- 3.6 本章小结
- 第四章 直线、曲线与弧线的绘制 ✔️
- 4.1 坐标轴的创建(上篇)
- 4.1.1 D3 中的边距约定(中篇)
- 4.1.2 坐标轴的生成(中篇)
- 4.1.2.1 比例尺的声明(中篇)
- 4.1.2.2 坐标轴的添加(下篇)
- 4.1.2.3 轴标签的添加(下篇)
- 4.1.2.4 DIY 实战:在 Observable 平台实现折线图坐标轴的绘制 ✔️
- 4.2 D3 折线图的绘制(精译中 ⏳)
文章目录
- DIY 实战:在 Observable 平台实现 D3折线图坐标轴的绘制
- 1 需求描述
- 2 实现过程
- 3 单元测试示例
- 4 复盘与小结
《D3.js in Action》全新第三版封面
译者按
4.1.2 节介绍了很多坐标轴的新知识,除了在本地实际敲一遍代码,还应该有意识地在 Observable 平台进行同步实战。要想掌握 D3,窃以为这是必不可少的重要环节。
DIY 实战:在 Observable 平台实现 D3折线图坐标轴的绘制
1 需求描述
根据 4.1 小节介绍的内容,在 Observable
平台实现一版完整的 D3 折线图坐标轴,如图 1 所示:
【图 1 需要在 Observable 实现的折线图坐标轴效果】
2 实现过程
登录 Observable
官网 https://observablehq.com/,在默认工作空间下新建一个空白记事本,写上标题:
【图 2 在 Observable 新建一个空白记事本,并写上标题】
按照 D3 的边距约定,定义一个尺寸常量 sizes
:
sizes = {// Conform to D3's margin conventionconst [top, right, bottom, left] = [40, 170, 25, 40];const margin = { top, right, bottom, left };const [width, height] = [1000, 500];const innerWidth = width - margin.left - margin.right;const innerHeight = height - margin.top - margin.bottom;return {marginTop: top,marginLeft: left,width,height,innerWidth,innerHeight};
}
注意:这里根据边距尺寸的使用情况做了些优化,实际绘制中只需要用到 6 个尺寸,因此单独导出为 sizes
的相应属性值。
接着另起一个单元格,定义一个 SVG 元素,并将边距约定中会用到的各个尺寸导入进来:
svg = {// Conform to D3's margin conventionconst { marginLeft, marginTop, width, height } = sizes;// create SVG containerconst svg = d3.create("svg").attr("viewBox", `0, 0, ${width}, ${height}`);// create lineChart selectionconst lineChart = svg.append("g").attr("transform", `translate(${marginLeft}, ${marginTop})`);appendTimeAxis(lineChart);appendTemperatureAxis(lineChart);stylingLineChartAxes(lineChart);appendAxisLabel(svg);return svg.node();
}
然后分别实现绘制 SVG 折线图坐标轴需要的四个工具方法:时间轴的绘制、温度轴的绘制、通用样式的处理、以及纵轴标签的添加,分别对应 appendTimeAxis(lineChart)
、appendTemperatureAxis(lineChart)
、stylingLineChartAxes(lineChart)
与 appendAxisLabel(svg)
。在实现这四个方法之前,先将原始数据文件 weekly_temperature.csv
上传到记事本页面(经测试,数据集也可以直接用 await
语法得到,无需放在 IIFE
结构中):
data = await FileAttachment("weekly_temperature.csv").csv({ typed: true });
【图 3 将原始数据集上传到 Observable 记事本页面】
【图 4 经过处理的折线图数据集】
然后就可以实现上述四个子方法了。首先是时间轴的绘制方法 appendTimeAxis(lineChart)
:
function appendTimeAxis(lineChart) {const { innerWidth, innerHeight } = sizes;// create xScaleconst firstDate = new Date(2021, 0, 1, 0, 0, 0);const lastDate = d3.max(data, (d) => d.date);const xScale = d3.scaleTime().domain([firstDate, lastDate]).range([0, innerWidth]);// create bottom axisconst bottomAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%b"));// draw the bottom time axislineChart.append("g").attr("class", "axis-x").attr("transform", `translate(0, ${innerHeight})`).call(bottomAxis);// center the tick labelslineChart.selectAll(".axis-x text").attr("x", (curr) => {const nextMonth = curr.getMonth() + 1;const nextDate = new Date(2021, nextMonth, 1);return (xScale(nextDate) - xScale(curr)) / 2;}).attr("y", "10px");
}
这里需要注意一点,第 13 行其实可以赋给一个常量,并作为函数结果返回。这样做方便 SVG 节点对绘制出的坐标轴进行统一管理(本例中暂不考虑)。另外,关于日期格式化函数 d3.timeFormat()
的写法和功能,这次实战还做了些深入源码的发散工作,将在下一篇详细讲解,本篇只介绍 d3.timeFormat()
的单元测试方法。
再来练练垂直方向温度轴的绘制方法 appendTemperatureAxis(lineChart)
的实现:
function appendTemperatureAxis(lineChart) {const { innerHeight } = sizes;// create yScaleconst yScale = d3.scaleLinear().domain([0, d3.max(data, (d) => d.max_temp_F)]).range([innerHeight, 0]);const leftAxis = d3.axisLeft(yScale);// append vertical axislineChart.append("g").attr("class", "axis-y").attr("x", "-5px").call(leftAxis);
}
接着是字体样式的统一设置:
function stylingLineChartAxes(lineChart) {lineChart.selectAll(".axis-x text, .axis-y text").style("font-family", "Roboto, sans-serif").style("font-size", "14px");
}
最后添加温度轴的轴标签文本:
function appendAxisLabel(svg) {svg.append("text").attr("y", "20px").text("Temperature (°F)");
}
注意:SVG 的文本元素 <text>
在填入文本时不是用的 attr()
方法,而是直接调用 text()
。
最后按 Shift + Enter 查看 SVG 节点中的坐标轴:
【图 5 绘制在 Observable 上的折线图坐标轴实测效果】
3 单元测试示例
根据 【第 031 篇】 实现的单元测试模块,这里以 d3.timeFormat()
函数为例进行单元测试。
首先导入单元测试模块(自定义的 MyMocha
类,以及来自 Chai.js
的断言方法 expect
):
import { MyMocha as Mocha, expect } from "@anton-playground/combined-unit-tests"
然后编程 Observable
版的单元测试用例,分别测试一至十二月的短月份格式化情况:
suit = {const suit = new Mocha("测试 D3.js 的日期格式化函数 d3.timeFormat(specifier):");const describe = suit.describe.bind(suit);const it = suit.it.bind(suit);const monthArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];const formatShortMonth = d3.timeFormat("%b");const fn = (date) => monthArray[date.getMonth()];describe("当标识符 specifier 为 '%b':", () => {monthArray.forEach((expected, index) => {const iDate = new Date(2024, index, 1, 0, 0, 0);// check d3.timeFormatit(`给定日期 2024-${index + 1}-1, 应该得到 "${expected}"`, () => {const actual = formatShortMonth(iDate);expect(actual).to.equal(expected);});});});return await suit.showResults();
}
测试运行效果:
【图 6 用自定义测试类测试 d3.timeFormat() 方法】
4 复盘与小结
-
书中出于代码复用的考虑,将坐标轴的样式设置集中到一个 D3 选择集中;实测时发现,按功能拆成四个方法后,样式设置必须放到坐标轴的后面执行,也就是说本次实战中的模块拆分存在副作用(需要等坐标轴先绘制完毕才行)。如果不考虑顺序,则最好将字体设置分别写入
appendTemperatureAxis(lineChart)
与appendTemperatureAxis(lineChart)
内; -
绘制坐标轴并未使用熟悉的 D3 数据绑定写法:
d3.selectAll().data().join()
;而是通过定义坐标轴所需的比例尺间接关联数据集data
。 -
D3 的坐标轴生成器共有四个,其中的方向(
axisLeft
、axisRight
、axisTop
、axisBottom
)与坐标轴整体的坐标无关,方法名中的方向仅仅表示刻度线相对于坐标轴线的位置(因此水平日期轴需要平移,而温度轴不需要):axisLeft
:刻度线及刻度标签位于坐标轴线的 左侧;axisRight
:刻度线及刻度标签位于坐标轴线的 右侧;axisTop
:刻度线及刻度标签位于坐标轴线的 顶部;axisBottom
:刻度线及刻度标签位于坐标轴线的 底部;
-
书中对
d3.timeFormat
的说明不多,只给了 D3 的参考文档。实测时可以自定义一个日期转短月份的格式化函数,例如:const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const bottomAxis = d3.axisBottom(xScale).tickFormat(date => months[date.getMonth() + 1]);
至于最后一条为什么要写成 d3.timeFormat('%b')
,我会在下一篇文章中结合 D3 源码进行详细说明,敬请关注!
对 D3 及 Observable
平台感兴趣的朋友也可以访问本次实战的 Observable
页面:https://observablehq.com/@anton-playground/d3-line-chart-axes-with-customized-unit-test