跳到主要内容

比例尺与坐标轴

比例尺(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 数据可视化的核心基础设施。理解不同类型的比例尺及其适用场景,是创建有效可视化的基础。连续比例尺处理数值和时间数据,离散比例尺处理分类数据,量化比例尺将连续数据转换为离散类别。坐标轴基于比例尺自动生成,提供了丰富的配置选项来定制刻度、格式和样式。熟练掌握比例尺和坐标轴的使用,将为构建复杂可视化打下坚实基础。