Awesome-omni-skill d3-viz
使用 d3.js 创建交互式数据可视化。此技能应在创建自定义图表、图形、网络图、地理可视化或任何需要对视觉元素、转换或交互进行细粒度控制的复杂基于 SVG 的数据可视化时使用。用于标准图表库之外的定制可视化,无论是 React、Vue、Svelte、原生 JavaScript 还是任何其他环境。
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/d3-viz" ~/.claude/skills/diegosouzapw-awesome-omni-skill-d3-viz && rm -rf "$T"
skills/development/d3-viz/SKILL.mdD3.js 可视化
概述
此技能提供关于使用 d3.js 创建高级交互式数据可视化的指导。D3.js(Data-Driven Documents)擅长将数据绑定到 DOM 元素并对它们应用数据驱动的转换,以创建具有对每个视觉元素精确控制的自定义、出版质量的可视化效果。这些技术适用于任何 JavaScript 环境,包括原生 JavaScript、React、Vue、Svelte 和其他框架。
何时使用 d3.js
在以下情况下使用 d3.js:
- 需要独特视觉编码或布局的自定义可视化
- 具有复杂平移、缩放或刷选行为的交互式探索
- 网络/图可视化(力导向布局、树状图、层次结构、弦图)
- 地理可视化与自定义投影
- 需要流畅、协调转换的可视化
- 具有精细样式控制的出版质量图形
- 标准库中不可用的新颖图表类型
对于以下情况考虑替代方案:
- 3D 可视化 - 改用 Three.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'); // 按 ID、类或 DOM 元素选择 // 清除之前的内容 svg.selectAll("*").remove(); // 设置尺寸 const width = 800; const height = 400; const margin = { top: 20, right: 30, bottom: 40, left: 50 }; // 创建比例尺、轴和绘制可视化 // ... d3 代码在这里 ... } // 在数据更改时调用 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 })); } // 在 React 中: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)} // 在 Vue 中: v-for 指令遍历返回的数组 // 在原生 JS 中: 根据返回的数据手动创建元素
对于具有转换、交互或利用 d3 完整功能的复杂可视化,请使用模式 A。对于更简单的可视化或当您的框架更喜欢声明式渲染时,请使用模式 B。
3. 构建可视化代码
在您的绘图函数中遵循此标准结构:
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. 实现响应式尺寸调整
使可视化对容器大小做出响应:
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(); // 在组件卸载或元素移除时调用
或者使用 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() .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'));
折线图
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) { // 数据格式:包含源、目标和值的对象数组 // 示例:[{ 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"); }
热力图
热力图使用颜色在二维网格中编码值,用于显示类别间的模式:
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); }
饼图
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) .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; }
添加交互性
工具提示
// 创建工具提示 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"); });
缩放和平移
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"); // 可选:分派自定义事件供您的框架/应用程序监听 // window.dispatchEvent(new CustomEvent('chartClick', { detail: d })); }); }
过渡和动画
为视觉变化添加平滑过渡:
// 基本过渡 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 用于许多元素 // 使用四叉树进行碰撞检测 // 使用 d3.line().curve(d3.curveStep) 简化路径 // 为大列表实现虚拟滚动 // 使用 requestAnimationFrame 进行自定义动画
无障碍性
使可视化具有无障碍性:
// 添加 ARIA 标签 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
- d3 比例尺的完整指南及示例scale-reference.md
- D3 颜色调色板和推荐colour-schemes.md
assets/
包含样板模板:
- 基本图表的起始模板chart-template.js
- 带有工具提示、缩放和交互的模板interactive-template.js
- 用于测试的示例数据集sample-data.json
这些模板适用于原生 JavaScript、React、Vue、Svelte 或任何其他 JavaScript 环境。根据您的特定框架需要进行调整。
要使用这些资源,在需要特定可视化类型或模式的详细指导时,请阅读相关文件。