比例尺
🎯 引言
前面的例子里,柱子高度直接等于数据:数据是 120,柱子就画 120px。但如果数据是 12000 的销量呢?画 12000px 高的柱子,屏幕根本放不下。学会这篇文章,你能掌握 D3 的比例尺——一个把“数据值”自动换算成“像素值”的换算器。
🧱 比例尺是什么
比例尺(scale)就是一个函数:喂给它一个数据,它返回一个像素值。
这跟我们生活中的地图一个道理:地图上 1 厘米代表实际 10 公里。你不用背着卷尺去量地球,查比例换算一下就能在纸上画地图。
创建一个比例尺只需要告诉它两个范围:
const yScale = d3
.scaleLinear()
.domain([0, 10000]) // 数据的范围:0 到 10000
.range([300, 0]); // 像素的范围:300 到 0
domain:数据的取值范围(输入)。range:像素的对应范围(输出)。
创建好之后它就是普通函数,调用就能看到换算结果:
| 调用 | 含义 | 结果 |
|---|---|---|
yScale(0) | 最小数据 | 300(画布底部) |
yScale(5000) | 一半的数据 | 150(画布中间) |
yScale(10000) | 最大数据 | 0(画布顶部) |
注意看:数据 5000 是 0~10000 的一半,像素 150 也正好是 300~0 的一半——等比例换算,这就是 scaleLinear(线性比例尺)名字里“线性”的意思。
拖动滑块改变数据值,红色横线左端指着数据尺上的输入,右端指着像素尺上的输出——yScale 干的就是这个换算:
[300, 0],是反着的。因为 SVG 的 y 轴向下(顶部是 0、底部是 300),数据越大柱子应该越靠上,所以大数值要映射成小像素。如果写成 [0, 300],图表就上下颠倒了,这是初学者最常犯的错。🧱 scaleLinear:让 D3 自己算范围
实际项目中数据范围不固定,写死 [0, 10000] 不合适。让 d3.max 帮我们算出最大值:
const data = [120, 200, 150, 80, 70];
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data)]) // d3.max 算出 200
.range([300, 0])
.nice();
两个小帮手:
d3.max(data):返回数组最大值,类似的还有d3.min。.nice():把范围边界“取整”。比如最大值是 198 时,domain 会变成[0, 200],坐标轴刻度就是 0、50、100 这样整齐的数字,而不是 0、39.6、79.2。
🧱 scaleBand:把宽度平分给分类
柱状图的横轴是“周一、周二”这类分类,不是数字,没法做线性换算。这时用 scaleBand,它的工作很简单:把总宽度平均分给每个分类。
const xScale = d3
.scaleBand()
.domain(['周一', '周二', '周三', '周四', '周五'])
.range([0, 260])
.padding(0.2); // 柱子之间留 20% 的间隙
260px 分给 5 个分类,每个分类先分到一个 50px 的“格子”;padding(0.2) 表示格子里留 20% 做间隙,剩下 40px 才是柱子的宽度:
| 调用 | 结果 | 含义 |
|---|---|---|
xScale('周一') | 10 | “周一”柱子的起始位置 |
xScale('周三') | 110 | “周三”柱子的起始位置 |
xScale.bandwidth() | 40 | 每个柱子的宽度 |
以前我们要手写 i * 60 算位置、手写 40 当宽度,现在 xScale(d.day) 和 xScale.bandwidth() 全包了,改数据、改宽度都不用自己重新算。
拖动滑块调整 padding,直观看看“格子”(虚线框)和“柱子宽度”(实心)的变化:
🧱 scaleTime:处理日期
横轴是日期时用 scaleTime,domain 直接传 Date 对象:
const xScale = d3
.scaleTime()
.domain([new Date('2026-01-01'), new Date('2026-01-07')])
.range([0, 300]);
xScale(new Date('2026-01-04')); // 150,1 月 4 日正好在中间
时间比例尺最常配合坐标轴使用,D3 会自动按“天”生成合适的刻度:
⚖️ 三种比例尺怎么选
| 比例尺 | domain 类型 | 典型用途 |
|---|---|---|
scaleLinear | 连续数值 | y 轴(数值轴) |
scaleBand | 离散分类 | 柱状图的 x 轴 |
scaleTime | 日期对象 | 时间序列图表的横轴 |
记忆技巧:数值用 Linear,分类用 Band,时间用 Time。
🧾 小节总结
- 比例尺是“数据值 → 像素值”的换算函数,
domain定输入范围,range定输出范围。 - SVG 的 y 轴向下,y 方向比例尺的 range 要反着写
[height, 0]。 scaleLinear配d3.max自动算范围,配.nice()让刻度取整。scaleBand把宽度平分给分类,xScale('周一')拿位置、bandwidth()拿柱子宽度。scaleTime处理日期,domain 直接传Date对象。
❓ 知识问答
Q1:range 写反了会怎样?
比例尺照样工作,但映射关系会反过来,数值越大柱子越矮。发现图表“上下颠倒”时,先检查 range 是不是该写成 [height, 0]。
Q2:scaleBand 的 padding 取值范围是多少?
0 到 1 之间的小数,表示间隙占每个格子的比例。常用 0.1 到 0.3,太大会让柱子变得又细又挤。
Q3:数据是对象数组时,d3.max 怎么用?
传第二个参数告诉它取哪个字段:d3.max(data, d => d.value)。
Q4:比例尺能用来映射颜色吗?
可以,把 range 写成颜色数组即可,如 .range(['#e0f3ff', '#08589e']),数值越大颜色越深,常用于热力图。
🧪 小练习
给下面的数据创建两个比例尺,并在控制台验证换算结果:
- 用
scaleBand创建 x 轴比例尺,domain 为五个科目,range 为[0, 400]。 - 用
scaleLinear创建 y 轴比例尺,domain 为 0 到最高成绩,range 为[300, 0]。 - 打印
xScale('数学')、xScale.bandwidth()和yScale(88),观察输出是否符合预期。
const data = [
{ subject: '语文', score: 80 },
{ subject: '数学', score: 95 },
{ subject: '英语', score: 88 },
{ subject: '物理', score: 72 },
{ subject: '化学', score: 90 },
];
// 请在这里编写代码
🎉 恭喜你已经掌握比例尺这个核心零件!下一篇我们把它和坐标轴组合起来,画出一个带刻度的完整柱状图。
