首页
/ 用 D3.js 在 OpenMontage 中构建交互式数据可视化:agent-skills 全流程实战指南

用 D3.js 在 OpenMontage 中构建交互式数据可视化:agent-skills 全流程实战指南

2026-09-07 12:22:58作者:幸俭卉

本指南以 OpenMontage 仓库内 .agents/skills/d3-viz/SKILL.md 技能文档为骨架,系统讲解如何用 D3.js(Data-Driven Documents)创建定制化、出版级的交互数据可视化,并覆盖其在 React、Vue、Svelte 与原生 JavaScript 等任意环境下的落地路径。读完你将掌握 D3 的引入方式、两种集成模式、六类核心图表模式、交互与动画编排、比例尺选型以及性能与无障碍最佳实践,并能直接借助技能包附带的模板与参考资源快速开工。

技能定位:OpenMontage 中的 D3.js 技能包

OpenMontage 是一个「开源的 Agent 视频生产系统」,仓库通过 .agents/skills/ 目录管理数百个可被 AI 编程助手调用的一线技能(skill)文件。d3-viz 技能即是其中的一员,其定义在 .agents/skills/d3-viz/SKILL.md,文件头部 YAML frontmatter 明确给出了技能的触发契约:

name: d3-viz
description: Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment.

也就是说,当 Agent 需要处理「标准图表库覆盖不了的自定义可视化」时(例如自定义图表、网络/图谱、地理可视化、对视觉元素/过渡/交互有细粒度控制的需求),就应该调用本技能。从仓库的 skills/INDEX.md 可以看到它的归位:它被列入 Data Visualization 能力行,与 remotion-best-practices 共同服务于 creative/data-visualization.md 这张策略级技能;同时它也出现在 Diagrams 能力分组中,与 beautiful-mermaid 并列。

需要区分的是分工层次:skills/creative/data-visualization.md 负责「在视频场景里用数据讲故事」的策略决策——选图类型、排动画节奏、定标签与色彩规则;而 d3-viz 则负责「把它真正画出来」的实现细节,是策略落地的执行级技能。

何时使用 d3.js

应该用 D3 的场景

  • 需要独特视觉编码或布局的定制可视化(超越标准图表库能力);
  • 需要复杂平移(pan)、缩放(zoom)或刷选(brush)交互的探索式可视化
  • 网络 / 关系图可视化:力导向布局、树形图、层级结构、弦图(chord diagram);
  • 自定义投影的地理可视化
  • 需要平滑、编排队形(choreographed)过渡动画的可视化
  • 要求精细样式控制、达到出版级质量的图形
  • 标准库中没有的新颖图表类型

应该考虑替代方案的情况

  • 3D 可视化:应使用 Three.js(仓库内对应的技能链是 threejs-fundamentalsthreejs-animationthreejs-world-generation 等)。

核心工作流

1. 引入并设置 d3.js

在脚本顶部用 ES Module 方式引入(推荐):

import * as d3 from 'd3';

或者使用 CDN 版本(7.x):

<script src="https://d3js.org/d3.v7.min.js"></script>

引入后,所有模块(比例尺 scales、坐标轴 axes、图形生成器 shapes、过渡 transitions 等)都通过 d3 命名空间统一访问,这也是全文档代码均以 d3.xxx 为前缀的原因。

2. 选择集成模式

模式 A:直接 DOM 操作(大多数情况下推荐)

用 D3 直接选择 DOM 元素并以命令式方式操作,任何 JavaScript 环境通用:

function drawChart(data) {
  if (!data || data.length === 0) return;

  const svg = d3.select('#chart'); // Select by ID, class, or DOM element

  // Clear previous content
  svg.selectAll("*").remove();

  // Set up dimensions
  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };

  // Create scales, axes, and draw visualisation
  // ... d3 code here ...
}

// Call when data changes
drawChart(myData);

模式 B:声明式渲染(适合带模板的框架)

用 D3 只负责数据计算(比例尺、布局),元素渲染交给框架:

function getChartElements(data) {
  const xScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([0, 400]);

  return data.map((d, i) => ({
    x: 50,
    y: i * 30,
    width: xScale(d.value),
    height: 25
  }));
}

// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// In Vue: v-for directive over the returned array
// In vanilla JS: Create elements manually from the returned data

选择建议:需要利用 D3 完整能力(过渡、交互、复杂可视化)时选模式 A;可视化较简单、或框架更偏好声明式渲染时选模式 B。技能包中的两个模板分别对应这两种风格的 React 实现(详见文末「配套资源」)。

3. 结构化你的绘图代码

遵循统一的标准结构(空数据守卫 → 清空 → 定义尺寸 → 带边距的主分组 → 比例尺 → 坐标轴 → 数据绑定与元素创建):

function drawVisualization(data) {
  if (!data || data.length === 0) return;

  const svg = d3.select('#chart'); // Or pass a selector/element
  svg.selectAll("*").remove(); // Clear previous render

  // 1. Define dimensions
  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  // 2. Create main group with margins
  const g = svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

  // 3. Create scales
  const xScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.x)])
    .range([0, innerWidth]);

  const yScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.y)])
    .range([innerHeight, 0]); // Note: inverted for SVG coordinates

  // 4. Create and append axes
  const xAxis = d3.axisBottom(xScale);
  const yAxis = d3.axisLeft(yScale);

  g.append("g")
    .attr("transform", `translate(0,${innerHeight})`)
    .call(xAxis);

  g.append("g")
    .call(yAxis);

  // 5. Bind data and create visual elements
  g.selectAll("circle")
    .data(data)
    .join("circle")
    .attr("cx", d => xScale(d.x))
    .attr("cy", d => yScale(d.y))
    .attr("r", 5)
    .attr("fill", "steelblue");
}

// Call when data changes
drawVisualization(myData);

这套结构里的两个细节值得强调:

  • SVG 的 y 轴天然向下增长,因此线性比例尺的 range 必须写成 [innerHeight, 0] 以反转坐标,这是初学者最常见的坐标错位来源;
  • 采用 margin 惯例后,所有绘图逻辑都基于 g 分组内的 innerWidth / innerHeight,坐标轴与图形就不会与画布边缘打架。

4. 实现响应式尺寸

让可视化随容器尺寸变化:

function setupResponsiveChart(containerId, data) {
  const container = document.getElementById(containerId);
  const svg = d3.select(`#${containerId}`).append('svg');

  function updateChart() {
    const { width, height } = container.getBoundingClientRect();
    svg.attr('width', width).attr('height', height);

    // Redraw visualisation with new dimensions
    drawChart(data, svg, width, height);
  }

  // Update on initial load
  updateChart();

  // Update on window resize
  window.addEventListener('resize', updateChart);

  // Return cleanup function
  return () => window.removeEventListener('resize', updateChart);
}

// Usage:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // Call when component unmounts or element removed

或者用 ResizeObserver 更直接地监听容器:

function setupResponsiveChartWithObserver(svgElement, data) {
  const observer = new ResizeObserver(() => {
    const { width, height } = svgElement.getBoundingClientRect();
    d3.select(svgElement)
      .attr('width', width)
      .attr('height', height);

    // Redraw visualisation
    drawChart(data, d3.select(svgElement), width, height);
  });

  observer.observe(svgElement.parentElement);
  return () => observer.disconnect();
}

两种方案都返回清理函数(cleanup),在 React/Vue 组件卸载或元素移除时调用,避免事件监听器或观察器泄漏。skill 文档还特别提醒:若在 React 中重绘,务必把变化的依赖写进 useEffect 依赖数组(模板中即表现为 [data])。

常见可视化模式

技能文档收录了 7 类覆盖大多数定制场景的模式。下面按「数据形态 → 采用模式」的视角逐一给出完整可运行骨架。

柱状图(Bar chart)

适用于按类别比较数量。类别轴用 scaleBand(提供 bandwidth() 以得到柱子宽度),数值轴用 scaleLinear

function drawBarChart(data, svgElement) {
  if (!data || data.length === 0) return;

  const svg = d3.select(svgElement);
  svg.selectAll("*").remove();

  const width = 800;
  const height = 400;
  const margin = { top: 20, right: 30, bottom: 40, left: 50 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  const g = svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

  const xScale = d3.scaleBand()
    .domain(data.map(d => d.category))
    .range([0, innerWidth])
    .padding(0.1);

  const yScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([innerHeight, 0]);

  g.append("g")
    .attr("transform", `translate(0,${innerHeight})`)
    .call(d3.axisBottom(xScale));

  g.append("g")
    .call(d3.axisLeft(yScale));

  g.selectAll("rect")
    .data(data)
    .join("rect")
    .attr("x", d => xScale(d.category))
    .attr("y", d => yScale(d.value))
    .attr("width", xScale.bandwidth())
    .attr("height", d => innerHeight - yScale(d.value))
    .attr("fill", "steelblue");
}

// Usage:
// drawBarChart(myData, document.getElementById('chart'));

折线图(Line chart)

d3.line() 把数据点串成连续路径,适合表现随时间变化的趋势:

const line = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.value))
  .curve(d3.curveMonotoneX); // Smooth curve

g.append("path")
  .datum(data)
  .attr("fill", "none")
  .attr("stroke", "steelblue")
  .attr("stroke-width", 2)
  .attr("d", line);

curve 一族函数决定了插值方式(curveMonotoneX 保证不产生震荡过冲,是做平滑时间序列的首选);若想加「面积」语义,可在 d3-patterns.md 中找到 d3.area() + SVG 渐变(linearGradient)组合实现渐变面积图的完整写法。

散点图(Scatter plot)

在笛卡尔坐标系用圆点编码数据,并可用圆半径(size)与颜色(colour)做双通道冗余编码:

g.selectAll("circle")
  .data(data)
  .join("circle")
  .attr("cx", d => xScale(d.x))
  .attr("cy", d => yScale(d.y))
  .attr("r", d => sizeScale(d.size)) // Optional: size encoding
  .attr("fill", d => colourScale(d.category)) // Optional: colour encoding
  .attr("opacity", 0.7);

弦图(Chord diagram)

弦图以圆形布局展示实体之间的关系,用 ribbon 色带表示流。输入数据为 {source, target, value} 对象数组,需先转化为邻接矩阵:

function drawChordDiagram(data) {
  // data format: array of objects with source, target, and value
  // Example: [{ source: 'A', target: 'B', value: 10 }, ...]

  if (!data || data.length === 0) return;

  const svg = d3.select('#chart');
  svg.selectAll("*").remove();

  const width = 600;
  const height = 600;
  const innerRadius = Math.min(width, height) * 0.3;
  const outerRadius = innerRadius + 30;

  // Create matrix from data
  const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
  const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));

  data.forEach(d => {
    const i = nodes.indexOf(d.source);
    const j = nodes.indexOf(d.target);
    matrix[i][j] += d.value;
    matrix[j][i] += d.value;
  });

  // Create chord layout
  const chord = d3.chord()
    .padAngle(0.05)
    .sortSubgroups(d3.descending);

  const arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

  const ribbon = d3.ribbon()
    .source(d => d.source)
    .target(d => d.target);

  const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
    .domain(nodes);

  const g = svg.append("g")
    .attr("transform", `translate(${width / 2},${height / 2})`);

  const chords = chord(matrix);

  // Draw ribbons
  g.append("g")
    .attr("fill-opacity", 0.67)
    .selectAll("path")
    .data(chords)
    .join("path")
    .attr("d", ribbon)
    .attr("fill", d => colourScale(nodes[d.source.index]))
    .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());

  // Draw groups (arcs)
  const group = g.append("g")
    .selectAll("g")
    .data(chords.groups)
    .join("g");

  group.append("path")
    .attr("d", arc)
    .attr("fill", d => colourScale(nodes[d.index]))
    .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());

  // Add labels
  group.append("text")
    .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
    .attr("dy", "0.31em")
    .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
    .attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
    .text((d, i) => nodes[i])
    .style("font-size", "12px");
}

弦图是这套模式里最容易写错的:标签旋转、文本锚点、弧与色带半径——都依赖对角度制(radian)与 SVG 坐标系的换算;技能文档用注释明确给出了数据契约,方便直接套用。本技能目录下 sample-data.json 未直接含弦图数据,但仓库交互/网络类数据可在后续自行构造。

热力图(Heatmap)

用颜色编码二维网格中的数值,行列轴都使用 scaleBand,颜色轴使用 sequential 比例尺:

function drawHeatmap(data) {
  // data format: array of objects with row, column, and value
  // Example: [{ row: 'A', column: 'X', value: 10 }, ...]

  if (!data || data.length === 0) return;

  const svg = d3.select('#chart');
  svg.selectAll("*").remove();

  const width = 800;
  const height = 600;
  const margin = { top: 100, right: 30, bottom: 30, left: 100 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

  // Get unique rows and columns
  const rows = Array.from(new Set(data.map(d => d.row)));
  const columns = Array.from(new Set(data.map(d => d.column)));

  const g = svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

  // Create scales
  const xScale = d3.scaleBand()
    .domain(columns)
    .range([0, innerWidth])
    .padding(0.01);

  const yScale = d3.scaleBand()
    .domain(rows)
    .range([0, innerHeight])
    .padding(0.01);

  // Colour scale for values
  const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
    .domain([0, d3.max(data, d => d.value)]);

  // Draw rectangles
  g.selectAll("rect")
    .data(data)
    .join("rect")
    .attr("x", d => xScale(d.column))
    .attr("y", d => yScale(d.row))
    .attr("width", xScale.bandwidth())
    .attr("height", yScale.bandwidth())
    .attr("fill", d => colourScale(d.value));

  // Add x-axis labels
  svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`)
    .selectAll("text")
    .data(columns)
    .join("text")
    .attr("x", d => xScale(d) + xScale.bandwidth() / 2)
    .attr("y", -10)
    .attr("text-anchor", "middle")
    .text(d => d)
    .style("font-size", "12px");

  // Add y-axis labels
  svg.append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`)
    .selectAll("text")
    .data(rows)
    .join("text")
    .attr("x", -10)
    .attr("y", d => yScale(d) + yScale.bandwidth() / 2)
    .attr("dy", "0.35em")
    .attr("text-anchor", "end")
    .text(d => d)
    .style("font-size", "12px");

  // Add colour legend
  const legendWidth = 20;
  const legendHeight = 200;
  const legend = svg.append("g")
    .attr("transform", `translate(${width - 60},${margin.top})`);

  const legendScale = d3.scaleLinear()
    .domain(colourScale.domain())
    .range([legendHeight, 0]);

  const legendAxis = d3.axisRight(legendScale)
    .ticks(5);

  // Draw colour gradient in legend
  for (let i = 0; i < legendHeight; i++) {
    legend.append("rect")
      .attr("y", i)
      .attr("width", legendWidth)
      .attr("height", 1)
      .attr("fill", colourScale(legendScale.invert(i)));
  }

  legend.append("g")
    .attr("transform", `translate(${legendWidth},0)`)
    .call(legendAxis);
}

热力图示例演示了两种可复用的通用技巧:① 用 Array.from(new Set(...)) 去重构造行列域;② 用「逐像素画 1px 高的 rect + scaleLinear.invert()」手写连续渐变色图例——当内置 d3.legend 不可用时这是标准做法。

饼图(Pie chart)

d3.pie() 负责把数值数组转换为角度区间,d3.arc() 负责把角度渲染为扇形路径:

const pie = d3.pie()
  .value(d => d.value)
  .sort(null);

const arc = d3.arc()
  .innerRadius(0)
  .outerRadius(Math.min(width, height) / 2 - 20);

const colourScale = d3.scaleOrdinal(d3.schemeCategory10);

const g = svg.append("g")
  .attr("transform", `translate(${width / 2},${height / 2})`);

g.selectAll("path")
  .data(pie(data))
  .join("path")
  .attr("d", arc)
  .attr("fill", (d, i) => colourScale(i))
  .attr("stroke", "white")
  .attr("stroke-width", 2);

innerRadius 设为大于 0 的值即可变成环形图(donut)——这也是 skills/creative/data-visualization.md 中所推荐的在视频叙事里「5~6 片以内、超出部分并入 Other」的视觉形态。

力导向网络(Force-directed network)

力导向图适合展现节点关系,d3-force 通过仿真(simulation)在 tick 事件中不断更新节点坐标:

const simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).id(d => d.id).distance(100))
  .force("charge", d3.forceManyBody().strength(-300))
  .force("center", d3.forceCenter(width / 2, height / 2));

const link = g.selectAll("line")
  .data(links)
  .join("line")
  .attr("stroke", "#999")
  .attr("stroke-width", 1);

const node = g.selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("r", 8)
  .attr("fill", "steelblue")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

simulation.on("tick", () => {
  link
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);

  node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
});

function dragstarted(event) {
  if (!event.active) simulation.alphaTarget(0.3).restart();
  event.subject.fx = event.subject.x;
  event.subject.fy = event.subject.y;
}

function dragged(event) {
  event.subject.fx = event.x;
  event.subject.fy = event.y;
}

function dragended(event) {
  if (!event.active) simulation.alphaTarget(0);
  event.subject.fx = null;
  event.subject.fy = null;
}

注意三个力的分工:forceLink 约束连线距离、forceManyBody(负强度)产生排斥、forceCenter 把整体拉向画布中心。拖拽处理则通过固定 fx/fy 实现——把被拖节点的位置「钉住」,并在拖拽结束时解除钉住以交还仿真。

加入交互能力

Tooltip 提示框

提示框应创建在 SVG 之外(追加到 body),通过 CSS 定位跟随鼠标:

// Create tooltip div (outside SVG)
const tooltip = d3.select("body").append("div")
  .attr("class", "tooltip")
  .style("position", "absolute")
  .style("visibility", "hidden")
  .style("background-color", "white")
  .style("border", "1px solid #ddd")
  .style("padding", "10px")
  .style("border-radius", "4px")
  .style("pointer-events", "none");

// Add to elements
circles
  .on("mouseover", function(event, d) {
    d3.select(this).attr("opacity", 1);
    tooltip
      .style("visibility", "visible")
      .html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
  })
  .on("mousemove", function(event) {
    tooltip
      .style("top", (event.pageY - 10) + "px")
      .style("left", (event.pageX + 10) + "px");
  })
  .on("mouseout", function() {
    d3.select(this).attr("opacity", 0.7);
    tooltip.style("visibility", "hidden");
  });

关键点:pointer-events: none 保证 tooltip 自身不会抢走鼠标事件,避免 hover 闪烁。

缩放与平移(Zoom and pan)

通过 d3.zoom() 把用户的滚轮/拖拽操作编码为 transform(含 x、y、k),再整体应用到内容分组上:

const zoom = d3.zoom()
  .scaleExtent([0.5, 10])
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });

svg.call(zoom);

scaleExtent([0.5, 10]) 限定了缩放范围;注意 transform 应作用在内容分组 g 而不是整个 svg 上,否则坐标轴与图形会一起被放大。交互模板 interactive-template.jsx 中演示了如何把 margin 与缩放 transform 叠加(先 translate margin,再叠加 event.transform)。

点击交互

circles
  .on("click", function(event, d) {
    // Handle click (dispatch event, update app state, etc.)
    console.log("Clicked:", d);

    // Visual feedback
    d3.selectAll("circle").attr("fill", "steelblue");
    d3.select(this).attr("fill", "orange");

    // Optional: dispatch custom event for your framework/app to listen to
    // window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
  });

与框架联动时,可直接调用框架状态更新(React 的 setState),或派发自定义 DOM 事件 chartClick 让应用层统一监听,保持 D3 层与应用层的解耦。

过渡与动画

D3 过渡的核心心智模型是「先 .transition(),再写目标属性值」,过渡期间 D3 自动完成数值插值:

// Basic transition
circles
  .transition()
  .duration(750)
  .attr("r", 10);

// Chained transitions
circles
  .transition()
  .duration(500)
  .attr("fill", "orange")
  .transition()
  .duration(500)
  .attr("r", 15);

// Staggered transitions
circles
  .transition()
  .delay((d, i) => i * 50)
  .duration(500)
  .attr("cy", d => yScale(d.value));

// Custom easing
circles
  .transition()
  .duration(1000)
  .ease(d3.easeBounceOut)
  .attr("r", 10);

四种编舞手段各有用途:基础过渡做简单属性变化、链式过渡制造先后顺序、delay((d, i) => i * 50) 实现「逐元素依次入场」的瀑布感(与 skills/creative/data-visualization.md 里柱状图「从左到右 0.1s 交错升起」的视频动画规范直接呼应)、ease(d3.easeBounceOut) 提供弹性缓动。对于数据增删场景,d3-patterns.md 里还提供了完整的 enter / update / exit 三段式过渡与 attrTween 路径形变(path morphing)实现。

比例尺参考

比例尺负责把数据域(domain)映射到可视范围(range),是 D3 一切视觉编码的地基。技能文档按三大类组织:

定量比例尺(Quantitative)

// Linear scale
const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, 500]);

// Log scale (for exponential data)
const logScale = d3.scaleLog()
  .domain([1, 1000])
  .range([0, 500]);

// Power scale
const powScale = d3.scalePow()
  .exponent(2)
  .domain([0, 100])
  .range([0, 500]);

// Time scale
const timeScale = d3.scaleTime()
  .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
  .range([0, 500]);

scale-reference.md 中可以进一步查证每个比例的细节与适用场景,包括:

比例尺 关键特性 典型场景
scaleLinear 线性插值,支持 .invert() 最常用:坐标、柱长、位置
scalePow / scaleSqrt 指数变换;scaleSqrtexponent(0.5) 的简写 感知缩放、用面积/半径编码数值
scaleLog 对数变换,domain 必须恒为正 跨数量级数据(人口、GDP)
scaleTime 时间域线性尺,.nice() 取整时间刻度 时间序列、时间轴
scaleQuantize 连续输入 → 离散分桶 分级(low/medium/high)、热力分级
scaleQuantile 按分位数等分组 百分位分类、偏态数据
scaleThreshold 自定义阈值切分 温度档、成绩档(A/B/C/D/F)

定量尺的通用方法还有 .clamp(true)(限制输出不越界)、.nice()(将 domain 扩展为规整圆值)、.copy()(获得独立副本)与 ticks(n) / tickFormat(n, ".2f")(生成刻度与格式化函数)。

序数比例尺(Ordinal)

// Band scale (for bar charts)
const bandScale = d3.scaleBand()
  .domain(['A', 'B', 'C', 'D'])
  .range([0, 400])
  .padding(0.1);

// Point scale (for line/scatter categories)
const pointScale = d3.scalePoint()
  .domain(['A', 'B', 'C', 'D'])
  .range([0, 400]);

// Ordinal scale (for colours)
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);

scaleBand 产生有宽度的区间(配合 .bandwidth() 画矩形、.padding/.paddingInner/.paddingOuter/.align 控制内外部留白与对齐);scalePoint 只产出点位置(配合 .step() 得到点间距)。scaleOrdinal 则把离散类别映射到固定颜色集合,且同一输入永远返回同一输出,保证多次调用渲染一致性——这在图表随数据更新的场景里极其重要。

Sequential 比例尺

// Sequential colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues)
  .domain([0, 100]);

// Diverging colour scale
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
  .domain([-10, 0, 10]);

Sequential 把连续数据映射为连续渐变,diverging 则带「中间值/零点」语义,domain 需给三段 [min, 0, max]。颜色插值器的完整清单(单色相、多色相、色觉障碍安全等)以及 Okabe-Ito 等推荐色板见 colour-schemes.md

最佳实践

数据准备

可视化前先清洗数据——过滤无效值、按需排序、解析日期:

// Filter invalid values
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));

// Sort data if order matters
const sortedData = [...data].sort((a, b) => b.value - a.value);

// Parse dates
const parsedData = data.map(d => ({
  ...d,
  date: d3.timeParse("%Y-%m-%d")(d.date)
}));

性能优化

大数据集(>1000 个元素)时:

// Use canvas instead of SVG for many elements
// Use quadtree for collision detection
// Simplify paths with d3.line().curve(d3.curveStep)
// Implement virtual scrolling for large lists
// Use requestAnimationFrame for custom animations

核心思路是:DOM 元素数量是 SVG 渲染的瓶颈,超过约千级应切换到 canvas 或减少 DOM 节点;自定义动画通过 requestAnimationFrame 对齐帧率。

无障碍(Accessibility)

// Add ARIA labels
svg.attr("role", "img")
   .attr("aria-label", "Bar chart showing quarterly revenue");

// Add title and description
svg.append("title").text("Quarterly Revenue 2024");
svg.append("desc").text("Bar chart showing revenue growth across four quarters");

// Ensure sufficient colour contrast
// Provide keyboard navigation for interactive elements
// Include data table alternative

这与仓库创意层 data-visualization.md 的无障碍规则同源:不得仅靠颜色传达信息,需配合文本标签与图案作为冗余编码,图元间对比度不低于 3:1。

样式规范

// Define colour palettes upfront
const colours = {
  primary: '#4A90E2',
  secondary: '#7B68EE',
  background: '#F5F7FA',
  text: '#333333',
  gridLines: '#E0E0E0'
};

// Apply consistent typography
svg.selectAll("text")
  .style("font-family", "Inter, sans-serif")
  .style("font-size", "12px");

// Use subtle grid lines
g.selectAll(".tick line")
  .attr("stroke", colours.gridLines)
  .attr("stroke-dasharray", "2,2");

配色建议直接抄作业:类别 ≤10 用 d3.schemeCategory10/d3.schemeTableau10;通用连续数据用 d3.interpolateViridis(感知均匀、打印安全、对色盲友好);围绕零点的发散数据用 d3.interpolateRdBud3.interpolateBrBG;对色觉障碍安全的发散色建议蓝-橙组合替代红-绿。完整的色彩语义(金融、温度、状态)、动态选色函数与 WCAG 对比度指引都在 colour-schemes.md 中,可当作随查随用的调色板手册。

常见问题与解决方案

现象 排查与修复
坐标轴不出现 检查 scale 的 domain 是否含 NaN;确认 axis 挂在正确的 group 上;检查 transform 平移是否正确
过渡不生效 确保 .transition() 写在属性变更之前;给元素提供唯一 key(对象恒常性);检查 useEffect 依赖是否包含变化的 data
响应式失效 使用 ResizeObserver 或 window resize 监听;把尺寸放进状态以触发重渲;确保 SVG 有 width/height 属性或 viewBox
性能问题 控制 DOM 元素数量(>1000 改用 canvas);对 resize 处理器做防抖;用 .join() 而非手写 enter/update/exit;核对依赖避免多余重渲染

其中「数据绑定需要唯一 key」这一条尤其隐蔽:没有 key 时 D3 按数组下标合并元素,数据顺序一旦变化,过渡就会作用到错误的元素上;写法是 .data(data, d => d.id)

配套资源:模板与参考文档

技能目录本身带有完整的引用材料,读取对应文件即可获得超出本技能正文的细节:

references/ 参考文档

  • d3-patterns.md:层叠图模式合集——树形图(d3.tree)、矩形树图(d3.treemap)、旭日图(d3.partition)、弦图、热力图、渐变面积图、堆叠柱状图、分组柱状图、气泡图、地图打点(d3.geoMercator + geoPath)、分级统计图(choropleth)、brush 刷选、跨图联动刷选(linked brushing)、enter/update/exit 过渡与路径形变。
  • scale-reference.md:D3 全量比例尺手册,含颜色插值(RGB/HSL/Lab/HCL)与「自适应选尺」(数据跨 >2 个数量级自动切 log)等实用组合。
  • colour-schemes.md:D3 色彩方案与调色板推荐,含色盲安全色板、语义色、行业风格(数据新闻/学术/商业)与常见配色误区清单。

assets/ 脚手架模板

  • chart-template.jsx:基础柱状图 React 模板。内部实现可见其结构完全遵循本技能正文的「margin 惯例 + band/linear 双尺度 + .join() 绑定 + 轴标签」骨架,并通过 useRef + useEffect([data]) 把 D3 命令式渲染接进 React 生命周期。
  • interactive-template.jsx:交互模板。叠加了 tooltip、缩放、点击选中高亮、入场动画,还把选中数据通过 React state(selectedPoint)回传到组件 UI,是「D3 负责图形、框架负责状态」混合架构的范本。
  • sample-data.json:测试用示例数据集,涵盖 timeSeries(时间序列)、categorical(类别)、scatterData(散点/气泡)、hierarchical(层级)、network(节点-边)、stackedData(堆叠)与 geographicPoints(地理经纬度)等多种形态,可直接喂给上述模板验证效果。

把 d3-viz 放进 OpenMontage 的数据叙事管线

最后回到仓库语境,串起这条完整链路:

  1. 策略层:当场景导演/剪辑需要「用数据讲故事」时,skills/creative/data-visualization.md 提供图表选型决策树(数量 3–9 个点优先柱状图/折线图、KPI 用 stat grid、超过 12 个点先聚合再画)、动画编舞(build-up / narrative highlight / comparison reveal)与 1080p 字号底线等视频专用规范;
  2. 实现层:当可视化需要「超越标准图表库的自定义形态」时,Agent 依据 frontmatter 契约触发 d3-viz 技能,按其核心工作流与七类图表模式写码,用其参考文档与模板加速交付——这正是 skills/INDEX.md 把 Data Visualization 行标记为 d3-viz, remotion-best-practices 的协作语义;
  3. 产出层:渲染出的图表既可作为独立 Web 可视化,也可接入仓库 remotion-composer 组件体系或通过 diagram_gen(见 tools/graphics/diagram_gen.py,支持基于 D3/Mermaid 生成图表)进入视频帧管线,实现从「数据 → 定制图表 → 视频画面」的一体化生产。

对需要在视频/页面里呈现复杂数据的 Agent 而言,把 .agents/skills/d3-viz/SKILL.md 作为首选执行手册,把文末的 references/ 与 assets/ 当作随取随用的代码库,就是一条成熟、可复现的 D3.js 可视化生产路径。

登录后查看全文
热门项目推荐
相关项目推荐