当前内容所在位置(可进入专栏查看其他译好的章节内容)
- 第一部分 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折线图坐标轴的绘制](#DIY 实战:在 Observable 平台实现 D3折线图坐标轴的绘制)
-
- [1 需求描述](#1 需求描述)
- [2 实现过程](#2 实现过程)
- [3 单元测试示例](#3 单元测试示例)
- [4 复盘与小结](#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
:
js
sizes = {
// Conform to D3's margin convention
const [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 元素,并将边距约定中会用到的各个尺寸导入进来:
js
svg = {
// Conform to D3's margin convention
const { marginLeft, marginTop, width, height } = sizes;
// create SVG container
const svg = d3.create("svg")
.attr("viewBox", `0, 0, ${width}, ${height}`);
// create lineChart selection
const 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
结构中):
js
data = await FileAttachment("weekly_temperature.csv")
.csv({ typed: true });
【图 3 将原始数据集上传到 Observable 记事本页面】
【图 4 经过处理的折线图数据集】
然后就可以实现上述四个子方法了。首先是时间轴的绘制方法 appendTimeAxis(lineChart)
:
js
function appendTimeAxis(lineChart) {
const { innerWidth, innerHeight } = sizes;
// create xScale
const 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 axis
const bottomAxis = d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%b"));
// draw the bottom time axis
lineChart.append("g")
.attr("class", "axis-x")
.attr("transform", `translate(0, ${innerHeight})`)
.call(bottomAxis);
// center the tick labels
lineChart.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)
的实现:
js
function appendTemperatureAxis(lineChart) {
const { innerHeight } = sizes;
// create yScale
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.max_temp_F)])
.range([innerHeight, 0]);
const leftAxis = d3.axisLeft(yScale);
// append vertical axis
lineChart.append("g")
.attr("class", "axis-y")
.attr("x", "-5px")
.call(leftAxis);
}
接着是字体样式的统一设置:
js
function stylingLineChartAxes(lineChart) {
lineChart
.selectAll(".axis-x text, .axis-y text")
.style("font-family", "Roboto, sans-serif")
.style("font-size", "14px");
}
最后添加温度轴的轴标签文本:
js
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
):
js
import { MyMocha as Mocha, expect } from "@anton-playground/combined-unit-tests"
然后编程 Observable
版的单元测试用例,分别测试一至十二月的短月份格式化情况:
js
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.timeFormat
it(`给定日期 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 的参考文档。实测时可以自定义一个日期转短月份的格式化函数,例如:jsconst 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