function BarChart(data, {
x = (d, i) => i,
y = d => d,
title,
marginTop = 20,
marginRight = 0,
marginBottom = 30,
marginLeft = 40,
width = 640,
height = 400,
xDomain,
xRange = [marginLeft, width - marginRight],
yType = d3.scaleLinear,
yDomain,
yRange = [height - marginBottom, marginTop],
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();
}