| name | d3-viz |
| description | 使用 d3.js 创建交互式数据可视化。此技能应在创建自定义图表、图形、网络图、地理可视化或任何需要对视觉元素、转换或交互进行细粒度控制的复杂基于 SVG 的数据可视化时使用。用于标准图表库之外的定制可视化,无论是 React、Vue、Svelte、原生 JavaScript 还是任何其他环境。 |
D3.js 可视化
概述
此技能提供关于使用 d3.js 创建高级交互式数据可视化的指导。D3.js(Data-Driven Documents)擅长将数据绑定到 DOM 元素并对它们应用数据驱动的转换,以创建具有对每个视觉元素精确控制的自定义、出版质量的可视化效果。这些技术适用于任何 JavaScript 环境,包括原生 JavaScript、React、Vue、Svelte 和其他框架。
何时使用 d3.js
在以下情况下使用 d3.js:
- 需要独特视觉编码或布局的自定义可视化
- 具有复杂平移、缩放或刷选行为的交互式探索
- 网络/图可视化(力导向布局、树状图、层次结构、弦图)
- 地理可视化与自定义投影
- 需要流畅、协调转换的可视化
- 具有精细样式控制的出版质量图形
- 标准库中不可用的新颖图表类型
对于以下情况考虑替代方案:
核心工作流程
1. 设置 d3.js
在脚本顶部导入 d3:
import * as d3 from 'd3';
或者使用 CDN 版本(7.x):
<script src="https://d3js.org/d3.v7.min.js"></script>
所有模块(比例尺、轴、形状、过渡等)都可以通过 d3 命名空间访问。
2. 选择集成模式
模式 A:直接 DOM 操作(推荐用于大多数情况)
使用 d3 选择 DOM 元素并以命令式方式操作它们。这适用于任何 JavaScript 环境:
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
}
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
}));
}
对于具有转换、交互或利用 d3 完整功能的复杂可视化,请使用模式 A。对于更简单的可视化或当您的框架更喜欢声明式渲染时,请使用模式 B。
3. 构建可视化代码
在您的绘图函数中遵循此标准结构:
function drawVisualization(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
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.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.([, innerWidth]);
yScale = d3.()
.([, d3.(data, d.)])
.([innerHeight, ]);
xAxis = d3.(xScale);
yAxis = d3.(yScale);
g.()
.(, )
.(xAxis);
g.()
.(yAxis);
g.()
.(data)
.()
.(, (d.))
.(, (d.))
.(, )
.(, );
}
(myData);
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);
drawChart(data, svg, width, height);
}
updateChart();
window.addEventListener('resize', updateChart);
return () => window.removeEventListener('resize', updateChart);
}
或者使用 ResizeObserver 进行更直接的容器监控:
function setupResponsiveChartWithObserver(svgElement, data) {
const observer = new ResizeObserver(() => {
const { width, height } = svgElement.getBoundingClientRect();
d3.select(svgElement)
.attr('width', width)
.attr('height', height);
drawChart(data, d3.select(svgElement), width, height);
});
observer.observe(svgElement.parentElement);
return () => observer.disconnect();
}
常见可视化模式
条形图
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()
.([, d3.(data, d.)])
.([innerHeight, ]);
g.()
.(, )
.(d3.(xScale));
g.()
.(d3.(yScale));
g.()
.(data)
.()
.(, (d.))
.(, (d.))
.(, xScale.())
.(, innerHeight - (d.))
.(, );
}
折线图
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
散点图
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))
.attr("fill", d => colourScale(d.category))
.attr("opacity", 0.7);
弦图
弦图在圆形布局中显示实体之间的关系,用带状表示它们之间的流动:
function drawChordDiagram(data) {
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;
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.);
j = nodes.(d.);
matrix[i][j] += d.;
matrix[j][i] += d.;
});
chord = d3.()
.()
.(d3.);
arc = d3.()
.(innerRadius)
.(outerRadius);
ribbon = d3.()
.( d.)
.( d.);
colourScale = d3.(d3.)
.(nodes);
g = svg.()
.(, );
chords = (matrix);
g.()
.(, )
.()
.(chords)
.()
.(, ribbon)
.(, (nodes[d..]))
.(, d3.((nodes[d..])).());
group = g.()
.()
.(chords.)
.();
group.()
.(, arc)
.(, (nodes[d.]))
.(, d3.((nodes[d.])).());
group.()
.( { d. = (d. + d.) / ; })
.(, )
.(, )
.(, d. > . ? : )
.( nodes[i])
.(, );
}
热力图
热力图使用颜色在二维网格中编码值,用于显示类别间的模式:
function drawHeatmap(data) {
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;
const rows = Array.from(new Set(data.map(d => d.row)));
const columns = Array.from(new Set(data.map(d => d.column)));
g = svg.()
.(, );
xScale = d3.()
.(columns)
.([, innerWidth])
.();
yScale = d3.()
.(rows)
.([, innerHeight])
.();
colourScale = d3.(d3.)
.([, d3.(data, d.)]);
g.()
.(data)
.()
.(, (d.))
.(, (d.))
.(, xScale.())
.(, yScale.())
.(, (d.));
svg.()
.(, )
.()
.(columns)
.()
.(, (d) + xScale.() / )
.(, -)
.(, )
.( d)
.(, );
svg.()
.(, )
.()
.(rows)
.()
.(, -)
.(, (d) + yScale.() / )
.(, )
.(, )
.( d)
.(, );
legendWidth = ;
legendHeight = ;
legend = svg.()
.(, );
legendScale = d3.()
.(colourScale.())
.([legendHeight, ]);
legendAxis = d3.(legendScale)
.();
( i = ; i < legendHeight; i++) {
legend.()
.(, i)
.(, legendWidth)
.(, )
.(, (legendScale.(i)));
}
legend.()
.(, )
.(legendAxis);
}
饼图
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);
力导向网络
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)
.(, dragended));
simulation.(, {
link
.(, d..)
.(, d..)
.(, d..)
.(, d..);
node
.(, d.)
.(, d.);
});
() {
(!event.) simulation.().();
event.. = event..;
event.. = event..;
}
() {
event.. = event.;
event.. = event.;
}
() {
(!event.) simulation.();
event.. = ;
event.. = ;
}
添加交互性
工具提示
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");
circles
.on("mouseover", function(event, d) {
d3.select(this).attr("opacity", 1);
tooltip
.style("visibility", "visible")
.html(`<strong>${d.label}</strong><br/>值: ${d.value}`);
})
.on("mousemove", function(event) {
tooltip
.(, (event. - ) + )
.(, (event. + ) + );
})
.(, () {
d3.().(, );
tooltip.(, );
});
缩放和平移
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
点击交互
circles
.on("click", function(event, d) {
console.log("点击了:", d);
d3.selectAll("circle").attr("fill", "steelblue");
d3.select(this).attr("fill", "orange");
});
}
过渡和动画
为视觉变化添加平滑过渡:
circles
.transition()
.duration(750)
.attr("r", 10);
circles
.transition()
.duration(500)
.attr("fill", "orange")
.transition()
.duration(500)
.attr("r", 15);
circles
.transition()
.delay((d, i) => i * 50)
.duration(500)
.attr("cy", d => yScale(d.value));
circles
.transition()
.duration(1000)
.ease(d3.easeBounceOut)
.attr("r", 10);
比例尺参考
定量比例尺
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
const logScale = d3.scaleLog()
.domain([1, 1000])
.range([0, 500]);
const powScale = d3.scalePow()
.exponent(2)
.domain([0, 100])
.range([0, 500]);
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 500]);
序数比例尺
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
序列比例尺
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
最佳实践
数据准备
始终在可视化之前验证和准备数据:
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
const sortedData = [...data].sort((a, b) => b.value - a.value);
const parsedData = data.map(d => ({
...d,
date: d3.timeParse("%Y-%m-%d")(d.date)
}));
性能优化
对于大型数据集(>1000 个元素):
无障碍性
使可视化具有无障碍性:
svg.attr("role", "img")
.attr("aria-label", "显示季度收入的条形图");
svg.append("title").text("2024 年季度收入");
svg.append("desc").text("显示四个季度收入增长的条形图");
样式
使用一致、专业的样式:
const colours = {
primary: '#4A90E2',
secondary: '#7B68EE',
background: '#F5F7FA',
text: '#333333',
gridLines: '#E0E0E0'
};
svg.selectAll("text")
.style("font-family", "Inter, sans-serif")
.style("font-size", "12px");
g.selectAll(".tick line")
.attr("stroke", colours.gridLines)
.attr("stroke-dasharray", "2,2");
常见问题及解决方案
问题:轴不出现
- 确保比例尺具有有效域(检查 NaN 值)
- 验证轴附加到正确的组
- 检查变换翻译是否正确
问题:过渡不起作用
- 在属性更改之前调用
.transition()
- 确保元素具有唯一的键以进行适当的数据绑定
- 检查 useEffect 依赖项是否包含所有更改的数据
问题:响应式尺寸调整不起作用
- 使用 ResizeObserver 或窗口调整大小监听器
- 更新状态中的尺寸以触发重新渲染
- 确保 SVG 具有宽度/高度属性或 viewBox
问题:性能问题
- 限制 DOM 元素的数量(>1000 项时考虑画布)
- 防抖调整大小处理程序
- 使用
.join() 而不是单独的进入/更新/退出选择
- 通过检查依赖项避免不必要的重新渲染
资源
references/
包含详细的参考资料:
d3-patterns.md - 可视化模式和代码示例的综合集合
scale-reference.md - d3 比例尺的完整指南及示例
colour-schemes.md - D3 颜色调色板和推荐
assets/
包含样板模板:
chart-template.js - 基本图表的起始模板
interactive-template.js - 带有工具提示、缩放和交互的模板
sample-data.json - 用于测试的示例数据集
这些模板适用于原生 JavaScript、React、Vue、Svelte 或任何其他 JavaScript 环境。根据您的特定框架需要进行调整。
要使用这些资源,在需要特定可视化类型或模式的详细指导时,请阅读相关文件。