【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

相关推荐
aPurpleBerry34 分钟前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v2 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记3 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜3 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=3 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css