比例尺与坐标轴
比例尺(Scales)是 D3 中最核心的概念之一。它的本质是一个映射函数,将数据空间(Data Space)的值转换为视觉空间(Visual Space)的值。无论是将数值映射到像素位置、将分类映射到颜色,还是将时间映射到坐标轴刻度,比例尺都是实现这些转换的基础工具。
理解比例尺的关键在于认清两个概念:定义域(Domain)是输入数据的范围,也就是原始数据的取值区间;值域(Range)是输出值的范围,通常是像素坐标、颜色或其他视觉属性。比例尺的工作就是在两者之间建立映射关系。
为什么需要比例尺?
在可视化中,数据值和视觉属性往往处于完全不同的量级。比如你有一组气温数据,范围是 -20°C 到 40°C,需要映射到图表的 Y 轴像素范围 0 到 500。如果直接使用数据值作为像素坐标,图表根本无法正常显示。比例尺解决了这个问题,它负责将数据值「翻译」成像素值。
更重要的是,比例尺让可视化变得灵活。当数据范围变化时,只需要更新比例尺的定义域,所有依赖该比例尺的图形都会自动更新。当图表尺寸变化时,只需要更新比例尺的值域。这种解耦让代码更加清晰和可维护。
连续比例尺
连续比例尺用于处理连续的数值数据,它们将连续的定义域映射到连续的值域。这是最常用的比例尺类型。
线性比例尺(Linear Scale)
线性比例尺是最基础、最常用的比例尺类型。它使用线性变换(y = mx + b)将定义域映射到值域,保持了数据的比例关系。
基础用法:
// 创建线性比例尺
const xScale = d3.scaleLinear()
.domain([0, 100]) // 定义域:数据范围 0-100
.range([0, 500]); // 值域:像素范围 0-500
// 使用比例尺进行映射
xScale(0); // 返回 0
xScale(50); // 返回 250
xScale(100); // 返回 500
// 超出定义域的值会被外推
xScale(150); // 返回 750(超出值域)
反向映射:
Y 轴通常需要反向映射,因为在 SVG 坐标系中,Y 值向下增加,但图表中数据值向上增加:
// Y 轴比例尺(注意 range 是反的)
const yScale = d3.scaleLinear()
.domain([0, 100])
.range([400, 0]); // 从底部 400 到顶部 0
yScale(0); // 返回 400(底部)
yScale(100); // 返回 0(顶部)
invert 方法:
invert 方法是映射的逆运算,将值域的值转换回定义域。这在交互中特别有用,比如根据鼠标位置获取对应的数据值:
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
// 正向映射
xScale(50); // 250
// 反向映射
xScale.invert(250); // 50
// 典型应用:根据鼠标位置获取数据值
svg.on('mousemove', function(event) {
const [mx] = d3.pointer(event);
const dataValue = xScale.invert(mx);
console.log('鼠标位置对应的数据值:', dataValue);
});
clamp 方法:
默认情况下,超出定义域的值会被外推到值域之外。使用 clamp 可以限制输出值始终在值域范围内:
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
scale(150); // 750(超出值域)
scale.clamp(true);
scale(150); // 500(被限制在值域最大值)
scale(-10); // 0(被限制在值域最小值)
nice 方法:
nice 方法会调整定义域的边界,使其变成更整齐的数值,便于显示美观的刻度:
// 原始数据范围不整齐
const scale = d3.scaleLinear()
.domain([0.241079, 0.969679])
.range([0, 500]);
scale.domain(); // [0.241079, 0.969679]
// 使用 nice 调整
scale.nice();
scale.domain(); // [0.2, 1]
// 可以指定刻度数量来控制精度
scale.nice(40);
scale.domain(); // [0.24, 0.98]
ticks 和 tickFormat 方法:
ticks 方法返回一组适合用作刻度的值,tickFormat 返回格式化函数:
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
// 获取约 10 个刻度值
scale.ticks(10); // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
// 获取格式化函数
const format = scale.tickFormat(10, '.1f');
format(25); // "25.0"
// 格式化为百分比
const percentFormat = scale.tickFormat(10, '+%');
percentFormat(50); // "+50%"
多段比例尺:
通过指定多个定义域和值域值,可以创建多段线性比例尺。这在创建发散色阶时特别有用:
// 创建发散颜色比例尺
const colorScale = d3.scaleLinear()
.domain([-1, 0, 1]) // 负值、零、正值
.range(['red', 'white', 'green']);
colorScale(-0.5); // 红白之间的粉红色
colorScale(0); // 白色
colorScale(0.5); // 白绿之间的浅绿色
rangeRound 方法:
当需要输出整数像素值时,使用 rangeRound 可以避免抗锯齿导致的模糊:
const scale = d3.scaleLinear()
.domain([0, 100])
.rangeRound([0, 500]);
scale(33.333); // 167(四舍五入到整数)
时间比例尺(Time Scale)
时间比例尺是线性比例尺的变体,专门用于处理时间数据。它使用 JavaScript 的 Date 对象作为定义域:
const xScale = d3.scaleTime()
.domain([new Date('2024-01-01'), new Date('2024-12-31')])
.range([0, 800]);
xScale(new Date('2024-07-01')); // 大约 400
// 时间比例尺的 ticks 方法可以返回合适的时间间隔
xScale.ticks(d3.timeMonth.every(1)); // 每月一个刻度
xScale.ticks(d3.timeWeek); // 每周一个刻度
xScale.ticks(10); // 大约 10 个刻度
// 时间格式化
const format = d3.timeFormat('%Y-%m-%d');
xScale.tickFormat(10, '%b')(new Date('2024-01-15')); // "Jan"
常用时间间隔:
d3.timeSecond // 秒
d3.timeMinute // 分钟
d3.timeHour // 小时
d3.timeDay // 天
d3.timeWeek // 周
d3.timeMonth // 月
d3.timeYear // 年
// 使用 every 方法创建自定义间隔
d3.timeDay.every(2) // 每 2 天
d3.timeMonth.every(3) // 每 3 个月
对数比例尺(Log Scale)
对数比例尺适用于数据范围跨越多个数量级的情况。它使用对数变换进行映射:
const logScale = d3.scaleLog()
.domain([1, 10000])
.range([0, 500]);
logScale(1); // 0
logScale(10); // 125
logScale(100); // 250
logScale(1000); // 375
logScale(10000); // 500
注意对数比例尺的定义域不能包含 0 或负数,因为 log(0) 和 log(负数) 是未定义的。如果数据包含 0 或负值,可以使用 symlog 比例尺。
Symlog 比例尺
Symlog(Symmetrical Log)比例尺是对数比例尺的改进,支持 0 和负值:
const symlogScale = d3.scaleSymlog()
.domain([-1000, 0, 1000])
.range([0, 500]);
symlogScale(-1000); // 0
symlogScale(0); // 250
symlogScale(1000); // 500
幂比例尺(Power Scale)
幂比例尺使用幂函数进行映射,适用于需要非线性缩放的场景:
// 平方比例尺
const powScale = d3.scalePow()
.exponent(2) // 指数为 2(平方)
.domain([0, 10])
.range([0, 100]);
powScale(5); // 25(5² = 25,映射到 25)
// 平方根比例尺(常用,有专门的方法)
const sqrtScale = d3.scaleSqrt()
.domain([0, 100])
.range([0, 50]);
// 常用于气泡图,确保面积与数值成正比
径向比例尺(Radial Scale)
径向比例尺专用于径向可视化,如径向柱状图。它在内部对值域进行平方处理,使输入值与输出面积的平方根成线性关系:
const radialScale = d3.scaleRadial()
.domain([0, 100])
.range([0, 200]);
// 常用于径向条形图或极坐标图表
离散比例尺
离散比例尺用于处理分类数据或离散值。
条形比例尺(Band Scale)
条形比例尺将离散的分类数据映射到连续的像素范围,是柱状图的核心:
const xScale = d3.scaleBand()
.domain(['苹果', '香蕉', '橙子', '葡萄']) // 分类数据
.range([0, 400]) // 像素范围
.padding(0.2); // 条形之间的间距
xScale('苹果'); // 返回条形的起始 x 坐标
xScale('香蕉');
xScale.bandwidth(); // 返回每个条形的宽度
padding 相关方法:
const scale = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([0, 300]);
// padding:同时设置内外间距
scale.padding(0.2);
// paddingInner:条形之间的间距
scale.paddingInner(0.1);
// paddingOuter:两端的外间距
scale.paddingOuter(0.3);
// align:对齐方式(0 = 左对齐,0.5 = 居中,1 = 右对齐)
scale.align(0.5);
round 方法:
round 方法确保条形宽度和位置都是整数,避免抗锯齿:
const scale = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([0, 300])
.round(true);
点比例尺(Point Scale)
点比例尺是条形比例尺的变体,将分类数据映射到点的位置而非区间:
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 300])
.padding(0.5); // 两端的间距
pointScale('A'); // 返回点 A 的 x 坐标
pointScale('B');
pointScale.step(); // 返回相邻点之间的距离
点比例尺和条形比例尺的区别:条形比例尺为每个分类分配一个区间(有宽度),点比例尺为每个分类分配一个精确位置(无宽度)。
序数比例尺(Ordinal Scale)
序数比例尺将离散值映射到离散值,常用于颜色映射:
const colorScale = d3.scaleOrdinal()
.domain(['苹果', '香蕉', '橙子'])
.range(['red', 'yellow', 'orange']);
colorScale('苹果'); // 'red'
colorScale('香蕉'); // 'yellow'
// 如果数据值不在定义域中,会循环使用值域
colorScale('葡萄'); // 'red'(循环回第一个颜色)
unknown 方法:
可以设置未知值的默认输出:
const scale = d3.scaleOrdinal()
.domain(['A', 'B'])
.range(['red', 'blue'])
.unknown('gray');
scale('C'); // 'gray'(未知值返回灰色)
量化比例尺
量化比例尺将连续数据转换为离散的类别。
量化比例尺(Quantize Scale)
将连续的定义域分割成均匀的区间,每个区间映射到值域中的一个值:
const quantizeScale = d3.scaleQuantize()
.domain([0, 100])
.range(['低', '中', '高']);
quantizeScale(10); // '低'
quantizeScale(50); // '中'
quantizeScale(90); // '高'
// 获取阈值
quantizeScale.thresholds(); // [33.33, 66.66]
分位数比例尺(Quantile Scale)
基于数据的实际分布来划分区间,确保每个区间包含大致相同数量的数据点:
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const quantileScale = d3.scaleQuantile()
.domain(data)
.range(['Q1', 'Q2', 'Q3', 'Q4']);
quantileScale(2); // 'Q1'
quantileScale(5); // 'Q2'
quantileScale(8); // 'Q4'
// 获取分位数阈值
quantileScale.quantiles(); // [3.25, 5.5, 7.75]
阈值比例尺(Threshold Scale)
自定义阈值来划分区间:
const thresholdScale = d3.scaleThreshold()
.domain([0, 50, 80]) // 阈值
.range(['不及格', '及格', '良好', '优秀']);
thresholdScale(-10); // '不及格'
thresholdScale(30); // '及格'
thresholdScale(65); // '良好'
thresholdScale(95); // '优秀'
颜色比例尺
D3 提供了专门用于颜色映射的比例尺和配色方案。
连续颜色比例尺
// 使用 d3-scale-chromatic 的配色方案
const colorScale = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateViridis);
colorScale(0); // 深紫色
colorScale(50); // 绿色
colorScale(100); // 黄色
// 其他内置配色
d3.interpolateWarm // 暖色调
d3.interpolateCool // 冷色调
d3.interpolatePlasma // 紫粉黄
d3.interpolateMagma // 黑红黄
d3.interpolateInferno // 黑红黄
d3.interpolateCividis // 色盲友好
发散颜色比例尺
用于有中心点的数据,如正负值:
const divergingScale = d3.scaleDiverging()
.domain([-1, 0, 1])
.interpolator(d3.interpolateRdYlGn);
divergingScale(-1); // 红色
divergingScale(0); // 黄色
divergingScale(1); // 绿色
内置配色方案
// 分类配色
d3.schemeCategory10 // 10 种颜色
d3.schemeAccent // 8 种颜色
d3.schemeDark2 // 8 种颜色
d3.schemePastel1 // 9 种淡色
d3.schemePastel2 // 8 种淡色
d3.schemeSet1 // 9 种颜色
d3.schemeSet2 // 8 种颜色
d3.schemeSet3 // 12 种颜色
// 使用示例
const color = d3.scaleOrdinal(d3.schemeCategory10);
坐标轴
坐标轴是基于比例尺自动生成的参考标记。D3 的 d3-axis 模块提供了四种方向的坐标轴生成器。
创建坐标轴
// 四种方向的坐标轴
const xAxis = d3.axisBottom(xScale); // 底部(刻度向下)
const yAxis = d3.axisLeft(yScale); // 左侧(刻度向左)
const xAxisTop = d3.axisTop(xScale); // 顶部(刻度向上)
const yAxisRight = d3.axisRight(yScale); // 右侧(刻度向右)
绘制坐标轴
const margin = {top: 30, right: 30, bottom: 50, left: 60};
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// 创建比例尺
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, 50])
.range([height, 0]);
// 绘制 X 轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale));
// 绘制 Y 轴
svg.append('g')
.call(d3.axisLeft(yScale));
刻度配置
ticks 方法:
设置刻度数量(只是一个建议值):
d3.axisBottom(xScale)
.ticks(5); // 大约 5 个刻度
// 对于时间比例尺,可以指定时间间隔
d3.axisBottom(timeScale)
.ticks(d3.timeMonth.every(2)); // 每 2 个月
// 同时指定数量和格式
d3.axisBottom(xScale)
.ticks(10, '.1f'); // 10 个刻度,保留 1 位小数
tickValues 方法:
显式指定刻度值:
d3.axisBottom(xScale)
.tickValues([0, 25, 50, 75, 100]);
tickFormat 方法:
自定义刻度文本格式:
// 使用 d3-format
d3.axisBottom(xScale)
.tickFormat(d => `${d}%`);
// 使用 d3.format
d3.axisBottom(xScale)
.tickFormat(d3.format(',.0f')); // 千位分隔符
// 货币格式
d3.axisLeft(yScale)
.tickFormat(d => `¥${d}`);
// SI 前缀格式(如 1K, 1M)
d3.axisLeft(yScale)
.tickFormat(d3.format('.2s'));
tickSize 方法:
设置刻度线长度:
// 设置所有刻度线长度
d3.axisBottom(xScale)
.tickSize(10);
// 分别设置内外刻度线
d3.axisBottom(xScale)
.tickSizeInner(6) // 内刻度线
.tickSizeOuter(12); // 外刻度线(轴线两端的)
// 设置为 0 隐藏刻度线
d3.axisBottom(xScale)
.tickSize(0);
tickPadding 方法:
设置刻度文本与轴线的距离:
d3.axisBottom(xScale)
.tickPadding(10); // 文本距离轴线 10 像素
坐标轴样式定制
D3 生成的坐标轴有特定的结构,可以通过 CSS 或 D3 方法进行样式定制:
坐标轴结构:
<g fill="none" font-size="10" font-family="sans-serif" text-anchor="middle">
<path class="domain" stroke="currentColor" d="M0.5,6V0.5H880.5V6"></path>
<g class="tick" opacity="1" transform="translate(0.5,0)">
<line stroke="currentColor" y2="6"></line>
<text fill="currentColor" y="9" dy="0.71em">0.0</text>
</g>
<!-- 更多 tick... -->
</g>
通过 CSS 定制:
/* 隐藏轴线 */
.axis .domain {
display: none;
}
/* 设置刻度线样式 */
.axis .tick line {
stroke: #ccc;
stroke-dasharray: 2, 2;
}
/* 设置刻度文本样式 */
.axis .tick text {
font-size: 12px;
fill: #666;
}
通过 D3 定制:
// 绘制坐标轴后进行样式定制
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.selectAll('text')
.attr('font-size', '12px')
.attr('fill', '#666');
// 移除轴线
svg.select('.x-axis .domain').remove();
// 移除刻度线
svg.select('.x-axis .tick line').remove();
网格线
网格线可以通过设置刻度线延伸到图表内部来实现:
// Y 轴网格线
svg.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale)
.tickSize(-width) // 负值让刻度线向右延伸
.tickFormat('') // 不显示文本
)
.selectAll('line')
.attr('stroke', '#e0e0e0')
.attr('stroke-dasharray', '3,3');
// 移除网格线的轴线
svg.select('.grid .domain').remove();
// X 轴网格线
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat('')
)
.selectAll('line')
.attr('stroke', '#e0e0e0');
动态更新坐标轴
当数据或比例尺变化时,可以重新调用坐标轴生成器来更新:
// 初始绘制
const xAxis = svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale));
// 更新比例尺
xScale.domain([0, 200]);
// 平滑过渡更新坐标轴
xAxis.transition()
.duration(750)
.call(d3.axisBottom(xScale));
完整示例
下面是一个综合示例,展示比例尺和坐标轴的完整用法:
// 数据
const data = [
{month: '一月', sales: 120},
{month: '二月', sales: 150},
{month: '三月', sales: 180},
{month: '四月', sales: 130},
{month: '五月', sales: 200},
{month: '六月', sales: 250}
];
// 尺寸设置
const margin = {top: 30, right: 30, bottom: 50, left: 60};
const width = 700 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
// 创建 SVG
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X 轴比例尺(条形比例尺)
const xScale = d3.scaleBand()
.domain(data.map(d => d.month))
.range([0, width])
.padding(0.2);
// Y 轴比例尺(线性比例尺)
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.sales) * 1.1])
.nice()
.range([height, 0]);
// 绘制网格线
svg.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat('')
)
.selectAll('line')
.attr('stroke', '#e0e0e0')
.attr('stroke-dasharray', '3,3');
svg.select('.grid .domain').remove();
// 绘制柱形
svg.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', d => xScale(d.month))
.attr('y', d => yScale(d.sales))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.sales))
.attr('fill', 'steelblue');
// 绘制 X 轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.selectAll('text')
.attr('font-size', '12px');
// 绘制 Y 轴
svg.append('g')
.call(d3.axisLeft(yScale)
.tickFormat(d => `${d}`)
)
.selectAll('text')
.attr('font-size', '12px');
// 添加 Y 轴标题
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', -45)
.attr('x', -height / 2)
.attr('text-anchor', 'middle')
.text('销售额(万元)')
.attr('font-size', '12px');
比例尺速查表
| 比例尺类型 | 用途 | 示例 |
|---|---|---|
scaleLinear | 连续数值映射 | 气温到像素 |
scaleTime | 时间数据映射 | 日期到坐标 |
scaleLog | 跨数量级数据 | 科学数据 |
scaleSymlog | 含零的对数数据 | 正负数范围 |
scalePow | 非线性缩放 | 强调差异 |
scaleSqrt | 面积映射 | 气泡图 |
scaleBand | 分类到区间 | 柱状图 X 轴 |
scalePoint | 分类到点 | 散点图 X 轴 |
scaleOrdinal | 分类到离散值 | 颜色映射 |
scaleQuantize | 连续到离散区间 | 分级显示 |
scaleSequential | 连续颜色映射 | 热力图 |
scaleDiverging | 发散颜色映射 | 正负值 |
总结
比例尺是 D3 数据可视化的核心基础设施。理解不同类型的比例尺及其适用场景,是创建有效可视化的基础。连续比例尺处理数值和时间数据,离散比例尺处理分类数据,量化比例尺将连续数据转换为离散类别。坐标轴基于比例尺自动生成,提供了丰富的配置选项来定制刻度、格式和样式。熟练掌握比例尺和坐标轴的使用,将为构建复杂可视化打下坚实基础。