chart = {
const width = 960;
const height = 500;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
const bins = d3.bin()
.thresholds(40)
.value((d) => d.rate)
(unemployment);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
/**
*
* 构建比例尺和坐标轴
* 主要使用 d3-scale 和 d3-axis 模块的 API
*
*/
// 设置横坐标轴的比例尺
// 使用方法 d3.scaleLinear() 构建一个线性比例尺
// 将数据映射为可视元素的属性时所使用的比例尺
// 具体可以参考 https://github.com/d3/d3-scale#linear-scales 或 https://d3js.org/d3-scale/linear
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-scale#线性比例尺-linear-scales
const x = d3.scaleLinear()
// 设置横坐标轴的定义域范围
// 以第一个分组 bin 的下限作为定义域范围的下限
// 以最后一个分组 bin 的上限作为定义域范围的上限
.domain([bins[0].x0, bins[bins.length - 1].x1])
// 设置横坐标轴的值域范围(所映射的可视元素)
// 以页面的宽度(扣除了外周的留白)作为范围,映射方向从左至右,和我们日常使用一致
.range([marginLeft, width - marginRight]);
// 设置纵坐标轴的比例尺,也是使用线性比例尺
const y = d3.scaleLinear()
// 设置纵坐标轴的定义域范围 [ymin, ymax]
// 使用 d3.max(iterable[, accessor]) 方法获取
.domain([0, d3.max(bins, (d) => d.length)])
// ⚠️ 应该特别留意纵坐标轴的值域(可视化属性,这里是长度)范围 [bottom, top]
// 由于 svg 的坐标体系中向下和向右是正方向,和我们日常使用的不一致
// 所以这里的值域范围需要采用从下往上与定义域进行映射
.range([height - marginBottom, marginTop]);
// 绘制横坐标轴
// 使用 svg.append("g") 在选择集 svg 的元素中(这个选择集只有 <svg> 元素),创建一个子元素 <g>
// 以其作为一个容器(包含坐标轴的轴线和坐标刻度以及坐标值),然后返回包含该元素的选择集(即此时的选择集已经改变了)
// 然后通过一系列的链式调用,主要是使用方法 selection.attr() 为选择集的元素(当前选择集包含的元素是 <g> 元素)设置属性
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`) // 将横坐标轴容器定位到底部
// 使用方法 d3.axisBottom(scale) 生成一个朝下的坐标轴(对象),即其刻度在水平轴线的下方
// 调用坐标轴对象方法 axis.ticks() 设置坐标轴刻度的间隔(一般是设置刻度的数量 count)
// 调用坐标轴对象方法 axis.tickSizeOuter([size]) 设置外侧刻度的长度,这里设为 0 就是取消坐标轴最外两侧的刻度线
// 关于坐标轴具体可以参考 d3-axis 模块 https://github.com/d3/d3-axis 或 https://d3js.org/d3-axis
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-scale#坐标轴
// 💡 另外值得留意的一点是这里使用的方法 selection.call(func)
// 💡 该方法会执行一次入参的函数 func,而且将选择集 selection 作为第一个入参传递给 func
// 💡 这里传入的参数是坐标轴对象 axis
// 💡 所以坐标轴对象实际上也是一个方法,接受一个 SVG 元素 context(一般是一个 <g> 元素),将坐标轴在其内部渲染出来
// 💡 最后返回当前选择集,这样是为了便于后续进行链式调用
// 💡 具体可以参考 https://github.com/d3/d3-selection#selection_call
// 💡 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-data-binding#其他方法
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
// 为坐标轴添加额外的标注信息(一般是刻度值的单位等信息)
.call((g) => g.append("text")
// 将该文本移动到坐标轴的右侧(即容器的右下角)
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end") // 设置文本的对齐方式
.text("Unemployment rate (%) →")); // 文本内容
// 绘制纵坐标轴
// Add the y-axis and label, and remove the domain line.
svg.append("g")
// 这里将纵坐标容器稍微往左移动一点,让坐标轴绘制在预先留出的 margin 区域中
.attr("transform", `translate(${marginLeft},0)`)
// 使用方法 d3.axisLeft(scale) 生成一个朝左的坐标轴(对象),即其刻度在垂直轴线的左方
.call(d3.axisLeft(y).ticks(height / 40))
// 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call((g) => g.select(".domain").remove())
// 为坐标轴添加额外的标注信息(一般是刻度值的单位等信息)
.call((g) => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Frequency (no. of counties)"));
/**
*
* 绘制直方图的柱子
*
*/
svg.append("g")
.attr("fill", "steelblue") // 设置柱子的颜色
// 使用 selection.selectAll() 进行「二次选择」,其作用是构建一个空的选择集
// 关于数据 D3 的数据绑定的流程可以参考以下链接里的笔记
// datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-data-binding#次级选择
.selectAll()
// 绑定数据
// 使用前面所生成的分组数据
// 这时候会创建一系列的「虚拟」占位元素添加到选择集里,它们与分组数据一一对应
.data(bins)
.join("rect") // 将元素绘制到页面上,使用 <rect> 元素来绘制柱子
// 为每个矩形分别设置左上角 (x, y) 及其 width 和 height 来确定其定位和形状
// 每个矩形条带的左上角横轴定位 x 由它所绑定的分组数据的下限 d.x0 所决定
// 通过横坐标轴的比例尺 x(d.x0) 进行映射求出具体的横轴坐标值
// 最后坐标轴还要加 1,相当于向右偏移 1 个像素点
// 这样每个条带之间就会由一个小间隔,以便它们之间可以区分开来
.attr("x", (d) => x(d.x0) + 1)
// 每个条带的宽度则由它所绑定的分组数据的上限 d.x1 和 d.x0 所决定
// 通过横坐标轴的比例尺 x(d.x1) 和 x(d.x0) 进行映射,分别求出具体的横轴坐标值,它们的差值就是条带的宽度
// 最后还需要减 1,以便在每个条带之间预留间隙
.attr("width", (d) => x(d.x1) - x(d.x0) - 1)
// 每个矩形条带的左上角纵轴定位 y 由它所绑定的分组所包含的元素个数 d.length 所决定
.attr("y", (d) => y(d.length))
// 每个条带的高度也是由所绑定的分组所包含的元素个数 d.length 所决定
.attr("height", (d) => y(0) - y(d.length));
// Return the SVG element.
return svg.node();
}