function BarChart(data, {
x = d => d,
y = (d, i) => i,
title,
titleColor = "white",
titleAltColor = "currentColor",
marginTop = 30,
marginRight = 0,
marginBottom = 10,
marginLeft = 30,
width = 640,
height,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft, width - marginRight],
xFormat,
xLabel,
yDomain,
// 这里默认就采用 [top, bottom]
yRange, // [top, bottom]
yPadding = 0.1, // 设置条形图中邻近柱子之间的间隔大小
color = "currentColor", // 柱子的颜色
} = {}) {
/**
*
* 处理数据
*
*/
// 通过 d3.map() 迭代函数,使用相应的 accessor function 访问函数从原始数据 data 中获取相应的值
const X = d3.map(data, x);
const Y = d3.map(data, y);
/**
*
* 构建比例尺和坐标轴
*
*/
// 计算坐标轴的定义域范围
// 横坐标轴的定义域 [xmin, xmax] 其中最大值 xmax 使用方法 d3.max(X) 从所有数据点的 X 值获取
if (xDomain === undefined) xDomain = [0, d3.max(X)];
// 如果调用函数时没有传入纵坐标轴的定义域范围 yDomain,则将其先设置为由所有数据点的 y 值构成的数组
if (yDomain === undefined) yDomain = Y;
// 然后基于 yDomain 值创建一个 InternSet 对象,以便去重
// 这样所得的 yDomain 里的元素都是唯一的,作为纵坐标轴的定义域(分类的依据)
yDomain = new d3.InternSet(yDomain);
// 这里还做了一步数据清洗
// 基于纵坐标轴的定义域所包含的类别
// 使用 JavaScript 数组的原生方法 arr.filter() 筛掉不属于 yDomain 类别的任意一个的数据点
// 其中 d3.range(X.length) 生成一个等差数列(使用 Y.length 也可以),作为索引值,便于对数据点进行迭代
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]));
// 如果调用函数时没有设置高度,则基于柱子的数量和上下的留白宽度算出默认的 svg 的高度
// 其中 yDomain.size 得到 InternSet 对象中包含的元素个数(即类别数量),然后这里假设每个柱子宽度是 25px
// 其中加上 yPadding 是为了考虑柱子间存在的间隔
// 通过 Math.ceil() 方法进行向上修约(让 svg 留足空间来绘制条形图)
if (height === undefined) height = Math.ceil((yDomain.size + yPadding) * 25) + marginTop + marginBottom;
// 然后计算出纵坐标轴的值域 [top, bottom]
if (yRange === undefined) yRange = [marginTop, height - marginBottom];
// 横坐标轴的数据是连续型的数值,默认使用 d3.scaleLinear 构建一个线性比例尺
const xScale = xType(xDomain, xRange);
// 横轴是一个刻度值朝上的坐标轴
// 并设置坐标轴的刻度数量和刻度值格式
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
// 纵坐标轴的数据是条形图的各种分类,使用 d3.scaleBand 构建一个带状比例尺
// 并设置间隔占据(柱子)区间的比例
const yScale = d3.scaleBand(yDomain, yRange).padding(yPadding);
// 纵轴是一个朝左的坐标轴
// 而且将坐标轴的外侧刻度 tickSizeOuter 长度设置为 0(即取消坐标轴首尾两端的刻度)
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
/**
*
* 柱子的标注信息的 accessor function 访问函数
* 统一为**基于索引**获取数据点的标注信息
*
*/
// 如果调用函数时没有设定标注信息的 accessor function 访问函数
// 则构建一个 accessor function 访问函数
// 它接受一个表示数据点的索引值,并从 X 中分别提取出柱子相应的频率组成标注信息
if (title === undefined) {
// 构建一个数值格式器(根据设置来自动确定数据的精度,更适用于阅读)
const formatValue = xScale.tickFormat(100, xFormat);
// 标注信息由该柱子相应的频率 formatValue(Y[i]) 组成
title = i => `${formatValue(X[i])}`;
} else {
// 如果调用函数时由设定标注信息的 accessor function 访问函数
// 为了便于后面统一基于索引值进行调用,需要进行转换
// 将 title 变成**基于索引**获取数据点的标注信息的 accessor function 访问函数
const O = d3.map(data, d => d);
const T = title; // 将原始的标注信息访问函数赋给 T 变量
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(0,${marginTop})`)
.call(xAxis) // 调用坐标轴(对象)方法,将坐标轴在相应容器内部渲染出来
.call(g => g.select(".domain").remove()) // 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.selectAll(".tick line").clone() // 这里复制了一份刻度线,用以绘制竖向的参考线
.attr("y2", height - marginTop - marginBottom) // 调整复制后的刻度线的终点位置(往下移动)
.attr("stroke-opacity", 0.1)) // 调小网格线的透明度
.call(g => g.append("text") // 为坐标轴添加额外信息名称(一般是刻度值的单位等信息)
// 将该文本移动到容器的右侧
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end") // 设置文本的对齐方式
.text(xLabel)); // 文本内容
// 绘制纵坐标轴
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`) // 将纵坐标轴容器定位到左侧
.call(yAxis);
/**
*
* 绘制条形图内的柱子
*
*/
svg.append("g")
.attr("fill", color)
// 使用 <rect> 元素来绘制柱子
// 通过设置矩形的左上角 (x, y) 及其 width 和 height 来确定其定位和形状
.selectAll("rect")
.data(I) // 绑定的数据是表示数据点的索引值(数组),以下会通过索引值来获取各柱子相应的数据
.join("rect")
// 因为绘制的是水平方向的条形图
// 所以每个柱子都是对齐到 y 轴的,即矩形的左上角横坐标值都是 xScale(0)
.attr("x", xScale(0))
// 通过索引值来读取矩形的左上角纵坐标值
.attr("y", i => yScale(Y[i]))
// 矩形的宽度
// 即水平柱子的长度,通过比例尺映射后,柱子的宽度是 xScale(X[i]) - xScale(0)) 的差值
.attr("width", i => xScale(X[i]) - xScale(0))
// 矩形的高度
// 即柱子的大小,通过纵轴的比例尺的方法 yScale.bandwidth() 获取 band 的宽度(不包含间隙 padding)
.attr("height", yScale.bandwidth());
/**
*
* 设置每个柱子的标注信息
* 根据柱子的长短,决定标注文本的不同位置和颜色
* 如果柱子较长,将标注信息置于柱子上,文本颜色为白色
* 如果柱子较短,将标注信息置于柱子旁边(在条形图的背景上),文本的颜色为黑色
*/
// 之前在垂直条形图中,只有当鼠标 hover 在柱子上才显示的标注信息,
// 💡 而现在由于条形图是横向的,与文字阅读方向相同,所以现在有足够的位置可以直接显示在相应的柱子上
svg.append("g")
// 先默认标注信息都在柱子上,设置文字的颜色
.attr("fill", titleColor)
// 因为默认文本在柱子的最右侧,所以对齐方式设置为 end
// 即文本的 (x, y) 定位坐标是其末尾,文字向左展开
.attr("text-anchor", "end")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("text")
.data(I) // 绑定的数据是表示数据点的索引值(数组),以下会通过索引值来获取各柱子相应标注信息
.join("text")
// 将文本移动到相应的柱子上
.attr("x", i => xScale(X[i])) // 文本的横向坐标,移到柱子的最右侧
.attr("y", i => yScale(Y[i]) + yScale.bandwidth() / 2) // 文本的纵向坐标
.attr("dy", "0.35em")
.attr("dx", -4)
.text(title)
// 最后再基于柱子的长度对文本的定位和颜色进行调整
.call(text =>
// 筛选出较短的柱子所对应的文本
// 当矩形的长度 xScale(X[i]) - xScale(0) 小于 20 时就是较短的柱子
text.filter(i => xScale(X[i]) - xScale(0) < 20) // short bars
.attr("dx", +4) // 将文本稍微向右移动,这样文本就位于条形图的白色背景上(而不是彩色的柱子上)
.attr("fill", titleAltColor) // 所以需要将白色的文字改成黑色的文字
// 而且改变文字的对齐方式为 start,即文本的 (x, y) 定位坐标是其开头,文字向右展开
.attr("text-anchor", "start")
);
return svg.node();
}