【D3.js in Action 3 精译_036】4.1 DIY 实战:在 Observable 平台实现 D3折线图坐标轴的绘制

当前内容所在位置(可进入专栏查看其他译好的章节内容)

  • 第一部分 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 的坐标轴生成器共有四个,其中的方向(axisLeftaxisRightaxisTopaxisBottom)与坐标轴整体的坐标无关,方法名中的方向仅仅表示刻度线相对于坐标轴线的位置(因此水平日期轴需要平移,而温度轴不需要):

    • axisLeft:刻度线及刻度标签位于坐标轴线的 左侧
    • axisRight:刻度线及刻度标签位于坐标轴线的 右侧
    • axisTop:刻度线及刻度标签位于坐标轴线的 顶部
    • axisBottom:刻度线及刻度标签位于坐标轴线的 底部
  • 书中对 d3.timeFormat 的说明不多,只给了 D3 的参考文档。实测时可以自定义一个日期转短月份的格式化函数,例如:

    js 复制代码
    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

相关推荐
十一吖i2 小时前
vue3表格显示隐藏列全屏拖动功能
前端·javascript·vue.js
徐同保3 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
生莫甲鲁浪戴3 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡5 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
拉不动的猪6 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
桃子不吃李子7 小时前
nextTick的使用
前端·javascript·vue.js
Devil枫8 小时前
HarmonyOS鸿蒙应用:仓颉语言与JavaScript核心差异深度解析
开发语言·javascript·ecmascript
惺忪97989 小时前
回调函数的概念
开发语言·前端·javascript
小二·9 小时前
从零开始:使用 Vue-ECharts 实现数据可视化图表功能
vue.js·信息可视化·echarts
前端 贾公子9 小时前
Element Plus组件v-loading在el-dialog组件上使用无效
前端·javascript·vue.js