Overview
This skill provides guidance on creating advanced interactive data visualizations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to them to create custom, publication-quality visualizations with precise control over every visual element. These techniques apply to any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks.
When to Use d3.js
Use d3.js when:
- Custom visualizations that require unique visual encodings or layouts
- Interactive exploration with complex pan, zoom, or brush behaviors
- Network/graph visualizations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
- Geographic visualizations with custom projections
- Visualizations that require smooth, coordinated transitions
- Publication-quality graphics with fine-grained styling control
- Novel chart types not available in standard libraries
Consider alternatives for:
- 3D visualization - use Three.js instead
Core Workflow
1. Set Up d3.js
Import d3 at the top of your script:
import * as d3 from 'd3';
Or use the CDN version (7.x):
<script src="https://d3js.org/d3.v7.min.js"></script>
All modules (scales, axes, shapes, transitions, etc.) are accessible through the d3 namespace.
2. Choose an Integration Pattern
Pattern A: Direct DOM Manipulation (Recommended for Most Cases) Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // 按 ID、类或 DOM 元素选择
// 清除之前的内容
svg.selectAll("*").remove();
// 设置尺寸
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
// 创建比例尺、轴和绘制可视化
// ... d3 代码在这里 ...
}
// 在数据更改时调用
drawChart(myData);
Pattern B: Declarative Rendering (For Frameworks with Templates) Use d3 for data computation (scales, layouts), but render elements through your framework:
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
}));
}
// 在 React 中: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// 在 Vue 中: v-for 指令遍历返回的数组
// 在原生 JS 中: 根据返回的数据手动创建元素
For complex visualizations with transitions, interactions, or leveraging the full power of d3, use Pattern A. For simpler visualizations or when your framework prefers declarative rendering, use Pattern B.
3. Structure the Visualization Code
Follow this standard structure in your drawing function:
function drawVisualization(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // 或传递选择器/元素
svg.selectAll("*").remove(); // 清除之前的渲染
// 1. 定义尺寸
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. 创建带边距的主要组
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 3. 创建比例尺
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]); // 注意:SVG 坐标系翻转
// 4. 创建并附加轴
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. 绑定数据并创建视觉元素
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");
}
// 在数据更改时调用
drawVisualization(myData);
4. Implement Responsive Sizing
Make the visualization respond to container size:
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);
}
// 用法:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // 在组件卸载或元素移除时调用
Or use ResizeObserver for more direct container monitoring:
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();
}
Common Visualization Patterns
Bar Chart
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");
}
// 用法:
// drawBarChart(myData, document.getElementById('chart'));
Line Chart
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);
Scatter Plot
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);
Chord Diagram
Chord diagrams show relationships between entities in a circular layout, with ribbons representing flows between them:
function drawChordDiagram(data) {
// 数据格式:包含源、目标和值的对象数组
// 示例:[{ 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;
// 从数据创建矩阵
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;
});
// 创建弦布局
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);
// 绘制带状
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());
// 绘制组(弧)
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());
// 添加标签
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");
}
Heatmap
Heatmaps use color to encode values in a two-dimensional grid, used to show patterns across categories:
function drawHeatmap(data) {
// 数据格式:包含行、列和值的对象数组
// 示例:[{ 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;
// 获取唯一行和列
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})`);
// 创建比例尺
const xScale = d3.scaleBand()
.domain(columns)
.range([0, innerWidth])
.padding(0.01);
const yScale = d3.scaleBand()
.domain(rows)
.range([0, innerHeight])
.padding(0.01);
// 值的颜色比例尺
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([0, d3.max(data, d => d.value)]);
// 绘制矩形
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));
// 添加 x 轴标签
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");
// 添加 y 轴标签
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");
// 添加颜色图例
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);
// 在图例中绘制颜色渐变
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);
}
Pie Chart
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);
Force-Directed Network
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;
}
Add Interactivity
Tooltips
// 创建工具提示 div(在 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");
// 添加到元素
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
.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");
});
Zoom and Pan
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
Click Interactions
circles
.on("click", function(event, d) {
// 处理点击(分派事件、更新应用程序状态等)
console.log("点击了:", d);
// 视觉反馈
d3.selectAll("circle").attr("fill", "steelblue");
d3.select(this).attr("fill", "orange");
// 可选:分派自定义事件供您的框架/应用程序监听
// window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
});
}
Transitions and Animations
Add smooth transitions to visual changes:
// 基本过渡
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);
Scale Reference
Quantitative Scales
// 线性比例尺
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]);
Ordinal Scales
// 带比例尺(用于条形图)
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);
Sequence Scales
// 序列颜色比例尺
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
// 发散颜色比例尺
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
Best Practices
Data Preparation
Always validate and prepare data before visualization:
// 过滤无效值
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)
}));
Performance Optimization
For large datasets (>1000 elements):
// 使用画布而不是 SVG 用于许多元素
// 使用四叉树进行碰撞检测
// 使用 d3.line().curve(d3.curveStep) 简化路径
// 为大列表实现虚拟滚动
// 使用 requestAnimationFrame 进行自定义动画
Accessibility
Make visualizations accessible:
// 添加 ARIA 标签
svg.attr("role", "img")
.attr("aria-label", "显示季度收入的条形图");
// 添加标题和描述
svg.append("title").text("2024 年季度收入");
svg.append("desc").text("显示四个季度收入增长的条形图");
// 确保足够的颜色对比度
// 为交互元素提供键盘导航
// 包含数据表替代方案
Styling
Use consistent, professional styling:
// 提前定义颜色调色板
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");
Common Problems and Solutions
Problem: Axes do not appear
- Ensure scales have valid domains (check for NaN values)
- Verify axes are attached to the correct groups
- Check that transform translations are correct
Problem: Transitions do not work
- Call
.transition() - Ensure elements have unique keys for proper data binding
- Check that useEffect dependencies include all changed data
Problem: Responsive resizing does not work
- Use ResizeObserver or a window resize listener
- Update dimensions in state to trigger re-rendering
- Ensure the SVG has width/height attributes or a viewBox
Problem: Performance issues
- Limit the number of DOM elements (consider canvas for >1000 items)
- Debounce resize handlers
- Use
.join()instead of separate enter/update/exit selections - Avoid unnecessary re-rendering by checking dependencies
Resources
references/
Includes detailed reference materials:
d3-patterns.md- Comprehensive collection of visualization patterns and code examplesscale-reference.md- Complete guide to d3 scales with examplescolour-schemes.md- D3 color palettes and recommendations
assets/
Includes boilerplate templates:
chart-template.js- Starter template for basic chartsinteractive-template.js- Templates with tooltips, zooming, and interactionsample-data.json- Sample datasets for testing
These templates work with native JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adjust according to your specific framework needs.
To use these resources, read the relevant files when you need detailed guidance on a specific visualization type or pattern.