Public
Edited
Jun 21, 2023
Insert cell
Insert cell
chart = BarChart(alphabet, {
x: d => d.letter,
y: d => d.frequency,
// 调用函数时,传入横轴的定义域 xDomain 手动设置分组类别
// 并且类别是按照其相应的频率进行降序排列的
// 使用方法 d3.groupSort(iterable, accessor, key) 对可迭代对象(如数组)iterable 进行归类分组,其中 key 指定分组的依据
// 最后返回(排序好的)类别数组,最终的输出值可以查看 👇 下一个 cell 所演示的结果
// 由于该方法默认按照 accessor 访问器的返回值升序排列,这里是希望降序排列,仅仅需要在返回值前面添加负号 - 即可
xDomain: d3.groupSort(alphabet, ([d]) => -d.frequency, d => d.letter),
yFormat: "%", // 纵轴的刻度值采用百分比表示
yLabel: "↑ Frequency",
width,
height: 500,
color: "steelblue"
})
Insert cell
d3.groupSort(alphabet, ([d]) => -d.frequency, d => d.letter)
Insert cell
alphabet = FileAttachment("alphabet.csv").csv({typed: true})
Insert cell
Insert cell
Insert cell
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/bar-chart
// 将绘制条形图的核心代码封装为一个函数(方便复用)
function BarChart(data, {
// 每个数据点的 x 值的 accessor function 访问函数
// 从数据点的原始值中提取出用作横坐标值(横坐标值应该采用 ordinal 离散型数据,以表示不同类别)
// 默认采用数据点的**序数**作为横坐标值
x = (d, i) => i,
// 每个数据点的 y 值的 accessor function 访问函数
// 从数据点的原始值中提取出用作纵坐标值(纵坐标值应该采用 quantitative 数值型数据,以表示具体定量的值)
y = d => d,
// 每个数据点的提示信息的 accessor function 访问函数,该函数的入参是各个数据点 d
title,
// 以下有一些关于图形的宽高、边距尺寸相关的参数
// margin 为前缀的产生是在外四边留白,构建一个显示的安全区,以便在四周显示坐标轴
marginTop = 20, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 30, // the bottom margin, in pixels
marginLeft = 40, // the left margin, in pixels
width = 640, // svg 的宽度
height = 400, // svg 的高度
// 横坐标轴的定义域范围,是一个数组,其中的每一个元素都是一个不同的类别
// 一般是基于原始数据(去重)提取而成的
// 也可以在这里手动直接设置希望显示的类别,然后在函数内部有相关的代码对数据进行筛选
xDomain,
// 横坐标轴的值域(可视化属性,这里是长度)范围 [left, right] 从左至右,和我们日常使用一致
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // 纵轴所采用的比例尺,对于数值型数据,默认采用线性比例尺
yDomain, // 纵坐标轴的定义域范围 [ymin, ymax]
// ⚠️ 应该特别留意纵坐标轴的值域(可视化属性,这里是长度)范围 [bottom, top]
// 由于 svg 的坐标体系中向下和向右是正方向,和我们日常使用的不一致
// 所以这里的值域范围需要采用从下往上与定义域进行映射
yRange = [height - marginBottom, marginTop], // [bottom, top]
// 设置条形图中邻近柱子之间的间隔大小
// 该参数是设置横轴值域的内外间隔系数,该值需要小于(或等于)1,表示留空的间隔占据(柱子)区间的比例
xPadding = 0.1,
yFormat, // 格式化数字的说明符 specifier 用于格式化纵坐标轴的刻度值
yLabel, // 为纵坐标轴添加额外文本(一般是刻度值的单位等信息)
color = "currentColor" // 柱子的颜色
} = {}) {
/**
*
* 处理数据
*
*/
// 通过 d3.map() 迭代函数,使用相应的 accessor function 访问函数从原始数据 data 中获取相应的值
const X = d3.map(data, x);
const Y = d3.map(data, y);

/**
*
* 构建比例尺和坐标轴
*
*/
// 计算坐标轴的定义域范围
// 如果调用函数时没有传入横坐标轴的定义域范围 xDomain,则将其先设置为由所有数据点的 x 值构成的数组
if (xDomain === undefined) xDomain = X;
// 然后基于 xDomain 值创建一个 InternSet 对象,以便去重
// 这样所得的 xDomain 里的元素都是唯一的,作为横坐标轴的定义域(分类的依据)
xDomain = new d3.InternSet(xDomain);
// 纵坐标轴的定义域 [ymin, ymax] 其中最大值 ymax 使用方法 d3.max(Y) 从所有数据点的 y 值获取
if (yDomain === undefined) yDomain = [0, d3.max(Y)];

// 这里还做了一步数据清洗
// 基于横坐标轴的定义域所包含的类别
// 使用 JavaScript 数组的原生方法 arr.filter() 筛掉不属于 xDomain 类别的任意一个的数据点
// 其中 d3.range(X.length) 生成一个等差数列(使用 Y.length 也可以),作为索引值,便于对数据点进行迭代
const I = d3.range(X.length).filter(i => xDomain.has(X[i]));
// 横坐标轴的数据是条形图的各种分类,使用 d3.scaleBand 构建一个带状比例尺
// 并设置间隔占据(柱子)区间的比例
const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
// 横轴是一个刻度值朝下的坐标轴
// 而且将坐标轴的外侧刻度 tickSizeOuter 长度设置为 0(即取消坐标轴首尾两端的刻度)
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);

// 纵坐标轴的数据是连续型的数值,默认使用 d3.scaleLinear 构建一个线性比例尺
const yScale = yType(yDomain, yRange);
// 纵轴是一个刻度值朝左的坐标轴
// 并设置坐标轴的刻度数量和刻度值格式
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);

/**
*
* 柱子的提示信息的 accessor function 访问函数
* 统一为**基于索引**获取数据点的提示信息
*
*/
// 如果调用函数时没有设定提示信息的 accessor function 访问函数
// 则构建一个 accessor function 访问函数
// 它接受一个表示数据点的索引值,并从 X 和 Y 中分别提取出柱子所属的类别和相应的频率组成提示信息
if (title === undefined) {
// 除了坐标轴对象(前面的 yAxis)可以设置刻度值格式外
// 比例尺本身也有与刻度值格式相关的方法 .tickFormat
// 但是这个方法返回值的不是比例尺(一般都是返回调用对象,即比例尺本身,便于进行链式调用)而是一个格式器
// 如果是连续型比例尺调用方法 continuous.tickFormat([count[, specifier]]) 则返回一个**数值格式器**
// 具体可以参考 https://github.com/d3/d3-scale#continuous_tickFormat
// 数值格式器设计初衷是对传入的值进行处理,转换得到一个值更适用于作为刻度值?
// 所以主要的作用是对传入的值进行修约(改变精度,让刻度值更易读),还可以进行格式转换(如变成百分比)
// 数值格式器会根据比例尺的定义域(该实例的定义域是 [0, 0.12702])自动决定刻度值的精度
// 第一个参数是设置预期的刻度线数量,刻度线越多,则刻度值的精度相对就会越高
// 第二个参数是设置刻度值的格式
const formatValue = yScale.tickFormat(100, yFormat);
// 提示信息由该柱子所属的类别 X[i] 及其相应的频率 formatValue(Y[i]) 组成
title = i => `${X[i]}\n${formatValue(Y[i])}`;
} else {
// 如果调用函数时由设定提示信息的 accessor function 访问函数
// 为了便于后面统一基于索引值进行调用,需要进行转换
// 将 title 变成**基于索引**获取数据点的提示信息的 accessor function 访问函数
const O = d3.map(data, d => d); // 实际就是原始数据
const T = title; // 将原始的提示信息访问函数赋给 T 变量
// 原始的访问函数 T 就是接收数据点的原始值 O[i] 作为参数,并返回相应的提示信息
title = i => T(O[i], i, data); // title 变成基于索引的提示信息 accessor function 访问函数
}

/**
*
* 绘制条形图框架(边框和坐标轴)
*
*/
// 创建 svg(返回的是一个包含 svg 元素的选择集)
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

// 绘制纵坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将纵向坐标轴容器「移动」到左侧
.attr("transform", `translate(${marginLeft},0)`)
// 调用坐标轴(对象)方法,将坐标轴在相应容器内部渲染出来
.call(yAxis)
.call(g => g.select(".domain").remove()) // 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.selectAll(".tick line").clone() // 这里复制了一份刻度线,用以绘制横向的参考线
.attr("x2", width - marginLeft - marginRight) // 调整复制后的刻度线的终点位置(往右移动)
.attr("stroke-opacity", 0.1)) // 调小参考线的透明度
.call(g => g.append("text") // 为坐标轴添加额外信息名称(一般是刻度值的单位等信息)
// 将该文本移动到容器的左上角
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start") // 设置文本的对齐方式
.text(yLabel)); // 文本内容

// 绘制横坐标轴
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`) // 将横坐标轴容器定位到底部
.call(xAxis);

/**
*
* 绘制条形图内的柱子
*
*/
const bar = svg.append("g")
.attr("fill", color) // 设置柱子的颜色
// 使用 <rect> 元素来绘制柱子
// 通过设置矩形的左上角 (x, y) 及其 width 和 height 来确定其定位和形状
.selectAll("rect")
.data(I) // 绑定的数据是表示数据点的索引值(数组),以下会通过索引值来获取各柱子相应的数据
.join("rect")
.attr("x", i => xScale(X[i])) // 通过索引值来读取柱子的左上角横坐标值
.attr("y", i => yScale(Y[i])) // 通过索引值来读取柱子的左上角纵坐标值
// 柱子的高度
// ⚠️ 应该特别留意因为在 svg 的坐标体系中向下和向右是正方向
// 所以通过比例尺映射后,在 svg 坐标体系里,柱子底部的 y 值(即 yScale(0))是大于柱子顶部的 y 值(即 yScale(Y[i])),所以柱子的高度是 yScale(0) - yScale(Y[i]) 的差值
.attr("height", i => yScale(0) - yScale(Y[i]))
// 柱子的宽度
// 通过横轴的比例尺的方法 xScale.bandwidth() 获取 band 的宽度(不包含间隙 padding)
// 这里不需要通过索引值来获取每个柱子的宽度,因为每一个柱子的宽度都相同
.attr("width", xScale.bandwidth());

// 设置每个柱子的提示信息
// 在每个柱子(容器)内分别添加一个 <title> 元素
// 当鼠标 hover 在柱子上时会显示相应的信息
if (title) bar.append("title")
// 这里通过提示信息的 accessor function 访问函数
// title 访问函数的入参是索引值(每个 bar 在上一步都绑定了数据,即相应的索引值)
.text(title);

return svg.node();
}
Insert cell
import {howto, altplot} from "@d3/example-components"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more