OpenMontage D3.js 比例尺(Scale)全参考:从连续映射到颜色编码的完整实战指南
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
本篇指南以 OpenMontage 仓库中 d3-viz 技能的 scale-reference.md 为骨架,系统梳理 D3.js 全部比例尺类型:连续比例尺(linear / power / log / time)、分箱比例尺(quantize / quantile / threshold)、颜色比例尺(sequential / diverging / sequentialQuantile)、序数比例尺(band / point / ordinal),以及.nice()、.clamp()、.ticks()等实用工具与颜色插值空间。读完你将能够为任意数据集选择合适的比例尺,写出坐标轴、柱状图、散点图、热力图和等值区域图(choropleth)中最核心的「数据→像素」映射代码。
什么是 D3 比例尺:一切可视化的第一块基石
D3.js(Data-Driven Documents)的核心思想是把数据绑定到 DOM 元素,并对元素施加数据驱动的变换。而**比例尺(scale)**正是数据值与视觉值之间那座桥:它把抽象的输入域(domain,如温度 0~100)映射到具体的输出范围(range,如屏幕像素 0~500),是坐标轴、条长、圆半径、颜色深浅等一切位置/大小/颜色编码的基础。
在 OpenMontage 中,d3-viz 技能(见 SKILL.md)被设计为「创建自定义图表、网络图、地理可视化或任何需要精细控制视觉元素的 SVG 可视化」时使用,可嵌入 React、Vue、Svelte 或原生 JavaScript 环境。配套的资源体系包括:
- scale-reference.md:本文讲解的完整比例尺参考;
- d3-patterns.md:树图、热力图、弦图、地理可视化等完整模式;
- colour-schemes.md:颜色方案与调色板推荐;
- chart-template.jsx 与 interactive-template.jsx:可直接运行的 React 模板;
- sample-data.json:时间序列、类别、散点、层级、网络等六类测试数据集。
下文将按「连续 → 颜色 → 序数 → 工具 → 插值 → 组合模式」的顺序完整展开。
连续比例尺:把连续数值映射到连续视觉通道
连续比例尺负责把连续输入域映射到连续输出范围,是量化数据可视化中使用最频繁的一族。
Linear scale:最通用的定量比例尺
d3.scaleLinear()以线性插值方式把连续输入域映射到连续输出范围,OpenMontage 的 d3-viz 技能将其列为「大多数定量数据的首选」。
const scale = d3.scaleLinear() .domain([0, 100]) .range([0, 500]); scale(50); // Returns 250 scale(0); // Returns 0 scale(100); // Returns 500 // Invert scale (get input from output) scale.invert(250); // Returns 50适用场景:
- 定量数据最常用的比例尺
- 坐标轴、柱条长度、位置编码
- 温度、价格、计数、测量值
核心方法:
.domain([min, max])— 设置输入域.range([min, max])— 设置输出范围.invert(value)— 由范围值反推域值.clamp(true)— 将输出限制在范围内.nice()— 将定义域扩展到整洁的取整值
一个值得注意的细节:当range使用字符串颜色时,linear scale 默认执行 RGB 插值(详见后文「颜色空间与插值」),这使它同时能充当最简单的连续色标。
Power scale:非线性感知缩放
d3.scalePow()以指数变换处理连续映射,通过.exponent()控制非线性程度;d3.scaleSqrt()则是指数为 0.5 的简写形式:
const sqrtScale = d3.scalePow() .exponent(0.5) // Square root .domain([0, 100]) .range([0, 500]); const squareScale = d3.scalePow() .exponent(2) // Square .domain([0, 100]) .range([0, 500]); // Shorthand for square root const sqrtScale2 = d3.scaleSqrt() .domain([0, 100]) .range([0, 500]);适用场景:
- 感知缩放(人类感知本身就是非线性的)
- 面积编码(用平方根把数值映射为圆的半径,避免面积被高估)
- 强调小值或大值之间的差异
Logarithmic scale:跨越多个数量级的数据
d3.scaleLog()以对数变换处理连续映射,非常适合人口、GDP、财富分布这类跨越多个数量级的数据:
const logScale = d3.scaleLog() .domain([1, 1000]) // Must be positive .range([0, 500]); logScale(1); // Returns 0 logScale(10); // Returns ~167 logScale(100); // Returns ~333 logScale(1000); // Returns 500适用场景:
- 跨越多个数量级的数据
- 人口、GDP、财富分布
- 对数坐标轴
- 指数增长可视化
重要约束:域值必须严格为正(>0)。
从代码实现角度看,scaleLinear内部存储domain与range两个数组,求值时先做双线性归一化再做线性插值;scaleLog则先把 domain 映射为Math.log10,再走同一套插值管线。理解这一共性后,你可以在需要时自行组合interpolate函数替换默认插值器(详见后文)。
Time scale:时间序列的专用线性比例尺
d3.scaleTime()是专为时间数据定制的线性比例尺,域由Date对象组成:
const timeScale = d3.scaleTime() .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) .range([0, 800]); timeScale(new Date(2022, 0, 1)); // Returns 400 // Invert to get date timeScale.invert(400); // Returns Date object for mid-2022适用场景:
- 时间序列可视化
- 时间线坐标轴
- 时间驱动的动画
- 基于日期的交互
方法:
.nice()— 将域扩展到整洁的时间间隔.ticks(count)— 生成间隔均匀的刻度值- 所有 linear scale 的方法同样适用
Quantize scale:连续输入 → 离散输出桶
d3.scaleQuantize()把连续输入映射到离散输出桶:输入域被等分成range.length段,每段对应一个输出值:
const quantizeScale = d3.scaleQuantize() .domain([0, 100]) .range(['low', 'medium', 'high']); quantizeScale(25); // Returns 'low' quantizeScale(50); // Returns 'medium' quantizeScale(75); // Returns 'high' // Get the threshold values quantizeScale.thresholds(); // Returns [33.33, 66.67]适用场景:
- 连续数据分箱
- 热力图颜色
- 风险等级(低/中/高)
- 年龄段、收入区间
Quantile scale:按分位数分组
d3.scaleQuantile()根据样本数据的分位数把连续输入映射为离散输出,其特点是每组元素数量大致相等,不受分布形态影响:
const quantileScale = d3.scaleQuantile() .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // Sample data .range(['low', 'medium', 'high']); quantileScale(8); // Returns based on quantile position quantileScale.quantiles(); // Returns quantile thresholds适用场景:
- 与分布无关的等量分组
- 基于百分位的分类
- 处理偏态分布
Threshold scale:自定义断点的分箱
d3.scaleThreshold()用自定义阈值把连续输入映射为离散输出。注意其规律:域中 n 个阈值把数轴切成 n+1 段,因此range长度总是domain.length + 1:
const thresholdScale = d3.scaleThreshold() .domain([0, 10, 20]) .range(['freezing', 'cold', 'warm', 'hot']); thresholdScale(-5); // Returns 'freezing' thresholdScale(5); // Returns 'cold' thresholdScale(15); // Returns 'warm' thresholdScale(25); // Returns 'hot'适用场景:
- 自定义断点
- 成绩等级(A、B、C、D、F)
- 温度类别
- 空气质量指数
顺序(Sequential)颜色比例尺:连续值 → 连续色带
顺序颜色比例尺把连续输入映射为连续颜色渐变,是热力图、等值区域图、密度图的核心编码手段。
Sequential colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues) .domain([0, 100]); colourScale(0); // Returns lightest blue colourScale(50); // Returns mid blue colourScale(100); // Returns darkest blue可用插值器:
单色相(Single hue):
d3.interpolateBlues、d3.interpolateGreens、d3.interpolateRedsd3.interpolateOranges、d3.interpolatePurples、d3.interpolateGreys
多色相(Multi-hue):
d3.interpolateViridis、d3.interpolateInferno、d3.interpolateMagmad3.interpolatePlasma、d3.interpolateWarm、d3.interpolateCoold3.interpolateCubehelixDefault、d3.interpolateTurbo
适用场景:
- 热力图、等值区域图(choropleth map)
- 连续数据可视化
- 温度、海拔、密度
补充说明:根据同技能包的 colour-schemes.md,interpolateViridis被推荐为通用首选——它感知均匀、对色盲友好、可安全打印且没有视觉死区;每个连续插值器都有对应的 9 步离散版本,例如d3.schemeBlues[9],可配合 quantize/threshold 使用。
Diverging colour scale:带中点的发散色带
d3.scaleDiverging()把连续输入映射为以中点为对称的发散渐变,用于表达「围绕某个有意义的中点(零、均值、中性值)偏离」的语义:
const divergingScale = d3.scaleDiverging(d3.interpolateRdBu) .domain([-10, 0, 10]); divergingScale(-10); // Returns red divergingScale(0); // Returns white/neutral divergingScale(10); // Returns blue可用插值器:
d3.interpolateRdBu— 红到蓝d3.interpolateRdYlBu— 红、黄、蓝d3.interpolateRdYlGn— 红、黄、绿d3.interpolatePiYG— 粉、黄、绿d3.interpolateBrBG— 棕、蓝绿d3.interpolatePRGn— 紫、绿d3.interpolatePuOr— 紫、橙d3.interpolateRdGy— 红、灰d3.interpolateSpectral— 彩虹光谱
适用场景:
- 具有语义中点的数据(零、均值、中性值)
- 正负值对比
- 高于/低于基准的比较
- 相关性矩阵
domain传入三个值[min, midpoint, max],中点对应的插值位置是 0.5(即色带中央颜色),非常适合表达盈亏、偏差与相关性。colour-schemes.md 还提醒:在做正负对比时,若无特殊语义要求,优先用蓝-橙(BrBG、PuOr)替代红-绿,以规避色盲问题。
Sequential quantile scale:顺序色 × 分位数
d3.scaleSequentialQuantile()把顺序色与分位数映射结合:它从样本域自动计算分位断点,再按分位位置分配颜色,天然抗离群值与偏态分布:
const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues) .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]); // Maps based on quantile position适用场景:
- 感知均匀的分箱
- 处理离群值
- 偏态分布
序数(Ordinal)比例尺:离散类别 → 位置或颜色
序数比例尺处理离散类别输入,是柱状图、散点图类别轴、图例与多系列图表的基石。
Band scale:为柱状图而生
d3.scaleBand()把离散输入映射为连续的「带」(矩形),支持内/外间距配置:
const bandScale = d3.scaleBand() .domain(['A', 'B', 'C', 'D']) .range([0, 400]) .padding(0.1); bandScale('A'); // Returns start position (e.g., 0) bandScale('B'); // Returns start position (e.g., 110) bandScale.bandwidth(); // Returns width of each band (e.g., 95) bandScale.step(); // Returns total step including padding bandScale.paddingInner(); // Returns inner padding (between bands) bandScale.paddingOuter(); // Returns outer padding (at edges)适用场景:
- 柱状图(最常见的用法)
- 分组元素
- 类别坐标轴
- 热力图单元格
间距选项:
.padding(value)— 同时设置内、外间距(0-1).paddingInner(value)— 带与带之间的间距(0-1).paddingOuter(value)— 边缘间距(0-1).align(value)— 带的对齐方式(0-1,默认 0.5)
bandwidth()与step()的区别值得强调:step = bandwidth + paddingInner × step,绘制rect宽度时务必使用bandwidth()而不是step()。
Point scale:类别位置的锚点
d3.scalePoint()把离散输入映射为连续的点位置(没有宽度),是折线图类别横轴、散点图类别轴与网络图节点定位的标准选择:
const pointScale = d3.scalePoint() .domain(['A', 'B', 'C', 'D']) .range([0, 400]) .padding(0.5); pointScale('A'); // Returns position (e.g., 50) pointScale('B'); // Returns position (e.g., 150) pointScale('C'); // Returns position (e.g., 250) pointScale('D'); // Returns position (e.g., 350) pointScale.step(); // Returns distance between points适用场景:
- 折线图的类别横轴
- 带类别轴的散点图
- 网络图中的节点位置
- 任何类别的点定位
Ordinal colour scale:类别 → 离散颜色
d3.scaleOrdinal()把离散输入映射为离散输出(颜色、形状等)。其确定性映射保证同一类别始终得到同一颜色:
const colourScale = d3.scaleOrdinal(d3.schemeCategory10); colourScale('apples'); // Returns first colour colourScale('oranges'); // Returns second colour colourScale('apples'); // Returns same first colour (consistent) // Custom range const customScale = d3.scaleOrdinal() .domain(['cat1', 'cat2', 'cat3']) .range(['#FF6B6B', '#4ECDC4', '#45B7D1']);内置颜色方案:
Categorical(分类):
d3.schemeCategory10— 10 色d3.schemeAccent— 8 色d3.schemeDark2— 8 色d3.schemePaired— 12 色d3.schemePastel1— 9 色d3.schemePastel2— 8 色d3.schemeSet1— 9 色d3.schemeSet2— 8 色d3.schemeSet3— 12 色d3.schemeTableau10— 10 色
适用场景:
- 类别颜色
- 图例项
- 多系列图表
- 网络节点类型
colour-schemes.md 补充了选型建议:一般用途默认schemeCategory10;仪表盘/商业报告可选schemeTableau10;超过 10 个类别时优先schemePaired(12 色)或对类别分组,而不是堆更多颜色。
比例尺工具:nice、clamp、copy 与刻度生成
所有比例尺都共享一组实用工具方法,本节完整列出。
Nice domain:扩展到整洁值
.nice()将 domain 扩展到「好看的」取整值,让坐标轴刻度更易读:
const scale = d3.scaleLinear() .domain([0.201, 0.996]) .nice(); scale.domain(); // Returns [0.2, 1.0] // With count (approximate tick count) const scale2 = d3.scaleLinear() .domain([0.201, 0.996]) .nice(5);可选参数count给出近似刻度数提示,scaleTime().nice()则会扩展到整洁的时间间隔。
Clamping:限制输出边界
.clamp(true)把超出域的输出严格限制在 range 范围内,防止散点或柱条溢出绘图区:
const scale = d3.scaleLinear() .domain([0, 100]) .range([0, 500]) .clamp(true); scale(-10); // Returns 0 (clamped) scale(150); // Returns 500 (clamped)Copy:独立副本
.copy()返回与原比例尺互不影响的副本,方便基于同一配置派生多个变体:
const scale1 = d3.scaleLinear() .domain([0, 100]) .range([0, 500]); const scale2 = scale1.copy(); // scale2 is independent of scale1Tick generation:坐标轴刻度
.ticks()生成整洁的刻度值,.tickFormat()返回对应的格式化函数,二者可直接喂给d3.axisBottom()/d3.axisLeft()使用:
const scale = d3.scaleLinear() .domain([0, 100]) .range([0, 500]); scale.ticks(10); // Generate ~10 ticks scale.tickFormat(10); // Get format function for ticks scale.tickFormat(10, ".2f"); // Custom format (2 decimal places) // Time scale ticks const timeScale = d3.scaleTime() .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]); timeScale.ticks(d3.timeYear); // Yearly ticks timeScale.ticks(d3.timeMonth, 3); // Every 3 months timeScale.tickFormat(5, "%Y-%m"); // Format as year-month时间比例尺的ticks()可接受d3.timeYear、d3.timeMonth等时间间隔对象,tickFormat支持%Y-%m这类 d3-time-format 语法。
颜色空间与插值:RGB、HSL、Lab、HCL
连续比例尺(尤其是 linear scale)的颜色插值方式可以通过.interpolate()切换,不同颜色空间影响过渡的「质感」:
const scale = d3.scaleLinear() .domain([0, 100]) .range(["blue", "red"]); // Default: RGB interpolationconst scale = d3.scaleLinear() .domain([0, 100]) .range(["blue", "red"]) .interpolate(d3.interpolateHsl); // Smoother colour transitionsconst scale = d3.scaleLinear() .domain([0, 100]) .range(["blue", "red"]) .interpolate(d3.interpolateLab); // Perceptually uniformconst scale = d3.scaleLinear() .domain([0, 100]) .range(["blue", "red"]) .interpolate(d3.interpolateHcl); // Perceptually uniform with hue- RGB(默认):分量线性插值,简单直接,但中间色可能发灰、不够「干净」;
- HSL:按色相角走捷径,过渡更平滑,但可能产生意外的色相环绕;
- Lab:感知均匀,中间色符合人眼亮度感知,适合追求专业过渡的场合;
- HCL:感知均匀且保留色相维度,是多色相渐变的首选。
在自定义发散/顺序渐变时,colour-schemes.md 中的做法也是同样的思路:d3.scaleLinear().range([...]).interpolate(d3.interpolateLab)即可得到感知均匀的自定义渐变。
常见组合模式:把比例尺用出生产力
本节是原文档的精华实战部分——六个高频组合模式,完整保留。
自定义中点的发散比例尺
const scale = d3.scaleLinear() .domain([min, midpoint, max]) .range(["red", "white", "blue"]) .interpolate(d3.interpolateHcl);多段渐变比例尺
const scale = d3.scaleLinear() .domain([0, 25, 50, 75, 100]) .range(["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#66c2a5"]);圆的半径比例尺(感知正确)
const radiusScale = d3.scaleSqrt() .domain([0, d3.max(data, d => d.value)]) .range([0, 50]); // Use with circles circle.attr("r", d => radiusScale(d.value));注意这里刻意使用scaleSqrt而不是scaleLinear:圆的面积与r²成正比,直接用线性比例尺会让大值圆显得「过大」。d3-patterns.md 的 Bubble chart 模式(sizeScale = d3.scaleSqrt().domain([0, d3.max(data, d => d.size)]).range([0, 50]))印证了这一约定。
基于数据范围的自适应比例尺
function createAdaptiveScale(data) { const extent = d3.extent(data); const range = extent[1] - extent[0]; // Use log scale if data spans >2 orders of magnitude if (extent[1] / extent[0] > 100) { return d3.scaleLog() .domain(extent) .range([0, width]); } // Otherwise use linear return d3.scaleLinear() .domain(extent) .range([0, width]); }d3.extent()一次性取回数据的最小值与最大值,配合比值判断(跨度超过两个数量级时切换为 log)即可实现通用自适应。colour-schemes.md 中还有类似的按数据正负动态选择 diverging/sequential 的写法,可与本模式结合。
带显式类别与未知值回退的颜色比例尺
const colourScale = d3.scaleOrdinal() .domain(['Low Risk', 'Medium Risk', 'High Risk']) .range(['#2ecc71', '#f39c12', '#e74c3c']) .unknown('#95a5a6'); // Fallback for unknown values.unknown()为未出现在 domain 中的输入提供回退色(如灰色「缺数据」),避免空值产生undefined或错误颜色。
项目侧印证:从 D3 比例尺到 Remotion 图表组件
OpenMontage 的视频渲染侧(remotion-composer)提供了不依赖 D3 的图表组件(BarChart、LineChart、PieChart、KPIGrid,见 charts 目录)。对照 BarChart.tsx 的实现,可以看到本文所述比例尺思想的「手工版」:
- 以
const maxValue = Math.max(...data.map((d) => d.value), 1);求出数据极值,等价于d3.max()与.domain([0, max]); - 用
barHeightFull = (datum.value / maxValue) * chartHeight做线性归一化,等价于scaleLinear().domain([0, max]).range([0, chartHeight]); - 网格线通过
value = (maxValue / gridLineCount) * i均分生成,等价于scale.ticks(5); - 柱宽由
barGap、barCount与可用宽度均摊,等价于scaleBand().paddingInner()。
这说明两件事:其一,掌握比例尺的本质(归一化 + 插值)后,即使在没有 D3 的渲染环境(如 Remotion 的逐帧渲染管线)里,也能手动复现同样的编码逻辑;其二,在需要交互、缩放、复杂布局或颜色插值的自定义可视化中,直接用 D3 比例尺仍是生产力最高的路径——这正是 d3-viz 技能及其 scale-reference.md 存在的意义。
实战速查与总结
按需求快速选型:
| 你的数据 | 推荐比例尺 |
|---|---|
| 定量数据 → 位置/长度 | scaleLinear(配.nice()、.clamp()) |
| 圆面积/感知缩放 | scaleSqrt或scalePow |
| 跨数量级数据 | scaleLog(domain 必须 >0) |
| 时间序列 | scaleTime(配ticks(d3.timeMonth)) |
| 连续值 → 少数几个桶 | scaleQuantize/scaleThreshold |
| 等量分组/抗偏态 | scaleQuantile |
| 连续值 → 色带 | scaleSequential(interpolateViridis) |
| 正负发散(含中点) | scaleDiverging(interpolateRdBu) |
| 类别 → 柱条 | scaleBand(用bandwidth()设宽) |
| 类别 → 点/折线轴 | scalePoint |
| 类别 → 颜色 | scaleOrdinal(schemeCategory10) |
最后的工程建议(综合本参考与 colour-schemes.md):先想清楚「输入是什么、输出编码什么通道」,再在连续/序数/颜色三大族中定位;坐标轴一律用.ticks()+.tickFormat()生成;需要颜色渐变时优先感知均匀的 Viridis/Cividis 或 Lab/HCL 插值;涉及色觉障碍用户时避免红-绿组合,并叠加形状、图案、标签等多重编码。掌握本文全部比例尺及其方法,你就能从数据直达像素,写出清晰、准确且专业的 D3 可视化。
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考