
D3 Viz
- 4 installs
- Updated January 15, 2026
- lionad-morotar/claude-d3js-skill-cn
Helps with ai & agent building tasks.
About
d3-viz is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- d3-viz
- AI & Agent Building
- AI-coding skill
D3 Viz by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,372 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lionad-morotar/claude-d3js-skill-cn --skill d3-vizAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| Last updated | January 15, 2026 |
| Repository | lionad-morotar/claude-d3js-skill-cn ↗ |
What it does
Helps with ai & agent building tasks.
Files
D3.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- 可视化模式和代码示例的综合集合scale-reference.md- d3 比例尺的完整指南及示例colour-schemes.md- D3 颜色调色板和推荐
assets/
包含样板模板:
chart-template.js- 基本图表的起始模板interactive-template.js- 带有工具提示、缩放和交互的模板sample-data.json- 用于测试的示例数据集
这些模板适用于原生 JavaScript、React、Vue、Svelte 或任何其他 JavaScript 环境。根据您的特定框架需要进行调整。
要使用这些资源,在需要特定可视化类型或模式的详细指导时,请阅读相关文件。
{
"cSpell.words": [
"chrisvoncsefalvay"
]
}import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
function BasicChart({ data }) {
const svgRef = useRef();
useEffect(() => {
if (!data || data.length === 0) return;
// 选择 SVG 元素
const svg = d3.select(svgRef.current);
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.label))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([innerHeight, 0])
.nice();
// 创建并附加轴
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// 绑定数据并创建视觉元素(在此示例中为条形)
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.label))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", "steelblue");
// 可选:添加轴标签
g.append("text")
.attr("class", "axis-label")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + margin.bottom - 5)
.attr("text-anchor", "middle")
.text("类别");
g.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -margin.left + 15)
.attr("text-anchor", "middle")
.text("值");
}, [data]);
return (
<div className="chart-container">
<svg
ref={svgRef}
width="800"
height="400"
style={{ border: '1px solid #ddd' }}
/>
</div>
);
}
// 示例用法
export default function App() {
const sampleData = [
{ label: 'A', value: 30 },
{ label: 'B', value: 80 },
{ label: 'C', value: 45 },
{ label: 'D', value: 60 },
{ label: 'E', value: 20 },
{ label: 'F', value: 90 }
];
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">基本 D3.js 图表</h1>
<BasicChart data={sampleData} />
</div>
);
}import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
function InteractiveChart({ data }) {
const svgRef = useRef();
const tooltipRef = useRef();
const [selectedPoint, setSelectedPoint] = useState(null);
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
// 尺寸
const width = 800;
const height = 500;
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)])
.range([0, innerWidth])
.nice();
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0])
.nice();
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.size || 10)])
.range([3, 20]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
// 添加缩放行为
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", `translate(${margin.left + event.transform.x},${margin.top + event.transform.y}) scale(${event.transform.k})`);
});
svg.call(zoom);
// 轴
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
const yAxisGroup = g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// 网格线
g.append("g")
.attr("class", "grid")
.attr("opacity", 0.1)
.call(d3.axisLeft(yScale)
.tickSize(-innerWidth)
.tickFormat(""));
g.append("g")
.attr("class", "grid")
.attr("opacity", 0.1)
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale)
.tickSize(-innerHeight)
.tickFormat(""));
// 工具提示
const tooltip = d3.select(tooltipRef.current);
// 数据点
const circles = 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 || 10))
.attr("fill", d => colourScale(d.category || 'default'))
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.attr("opacity", 0.7)
.style("cursor", "pointer");
// 悬停交互
circles
.on("mouseover", function(event, d) {
// 放大圆圈
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 1)
.attr("stroke-width", 3);
// 显示工具提示
tooltip
.style("display", "block")
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px")
.html(`
<strong>${d.label || '点'}</strong><br/>
X: ${d.x.toFixed(2)}<br/>
Y: ${d.y.toFixed(2)}<br/>
${d.category ? \`类别: \${d.category}<br/>\` : ''}
${d.size ? \`大小: \${d.size.toFixed(2)}\` : ''}
`);
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
// 恢复圆圈
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 0.7)
.attr("stroke-width", 2);
// 隐藏工具提示
tooltip.style("display", "none");
})
.on("click", function(event, d) {
// 突出显示选定的点
circles.attr("stroke", "#fff").attr("stroke-width", 2);
d3.select(this)
.attr("stroke", "#000")
.attr("stroke-width", 3);
setSelectedPoint(d);
});
// 在初始渲染时添加过渡
circles
.attr("r", 0)
.transition()
.duration(800)
.delay((d, i) => i * 20)
.attr("r", d => sizeScale(d.size || 10));
// 轴标签
g.append("text")
.attr("class", "axis-label")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + margin.bottom - 5)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("X 轴");
g.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -margin.left + 15)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Y 轴");
}, [data]);
return (
<div className="relative">
<svg
ref={svgRef}
width="800"
height="500"
style={{ border: '1px solid #ddd', cursor: 'grab' }}
/>
<div
ref={tooltipRef}
style={{
position: 'absolute',
display: 'none',
padding: '10px',
background: 'white',
border: '1px solid #ddd',
borderRadius: '4px',
pointerEvents: 'none',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
fontSize: '13px',
zIndex: 1000
}}
/>
{selectedPoint && (
<div className="mt-4 p-4 bg-blue-50 rounded border border-blue-200">
<h3 className="font-bold mb-2">选定的点</h3>
<pre className="text-sm">{JSON.stringify(selectedPoint, null, 2)}</pre>
</div>
)}
</div>
);
}
// 示例用法
export default function App() {
const sampleData = Array.from({ length: 50 }, (_, i) => ({
id: i,
label: `点 ${i + 1}`,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 30 + 5,
category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)]
}));
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-2">交互式 D3.js 图表</h1>
<p className="text-gray-600 mb-4">
悬停在点上查看详情。点击选择。滚动缩放。拖拽平移。
</p>
<InteractiveChart data={sampleData} />
</div>
);
}{
"timeSeries": [
{ "date": "2024-01-01", "value": 120, "category": "A" },
{ "date": "2024-02-01", "value": 135, "category": "A" },
{ "date": "2024-03-01", "value": 128, "category": "A" },
{ "date": "2024-04-01", "value": 145, "category": "A" },
{ "date": "2024-05-01", "value": 152, "category": "A" },
{ "date": "2024-06-01", "value": 168, "category": "A" },
{ "date": "2024-07-01", "value": 175, "category": "A" },
{ "date": "2024-08-01", "value": 182, "category": "A" },
{ "date": "2024-09-01", "value": 190, "category": "A" },
{ "date": "2024-10-01", "value": 185, "category": "A" },
{ "date": "2024-11-01", "value": 195, "category": "A" },
{ "date": "2024-12-01", "value": 210, "category": "A" }
],
"categorical": [
{ "label": "产品 A", "value": 450, "category": "电子产品" },
{ "label": "产品 B", "value": 320, "category": "电子产品" },
{ "label": "产品 C", "value": 580, "category": "服装" },
{ "label": "产品 D", "value": 290, "category": "服装" },
{ "label": "产品 E", "value": 410, "category": "食品" },
{ "label": "产品 F", "value": 370, "category": "食品" }
],
"scatterData": [
{ "x": 12, "y": 45, "size": 25, "category": "组 A", "label": "点 1" },
{ "x": 25, "y": 62, "size": 35, "category": "组 A", "label": "点 2" },
{ "x": 38, "y": 55, "size": 20, "category": "组 B", "label": "点 3" },
{ "x": 45, "y": 78, "size": 40, "category": "组 B", "label": "点 4" },
{ "x": 52, "y": 68, "size": 30, "category": "组 C", "label": "点 5" },
{ "x": 65, "y": 85, "size": 45, "category": "组 C", "label": "点 6" },
{ "x": 72, "y": 72, "size": 28, "category": "组 A", "label": "点 7" },
{ "x": 85, "y": 92, "size": 50, "category": "组 B", "label": "点 8" }
],
"hierarchical": {
"name": "根",
"children": [
{
"name": "类别 1",
"children": [
{ "name": "项目 1.1", "value": 100 },
{ "name": "项目 1.2", "value": 150 },
{ "name": "项目 1.3", "value": 80 }
]
},
{
"name": "类别 2",
"children": [
{ "name": "项目 2.1", "value": 200 },
{ "name": "项目 2.2", "value": 120 },
{ "name": "项目 2.3", "value": 90 }
]
},
{
"name": "类别 3",
"children": [
{ "name": "项目 3.1", "value": 180 },
{ "name": "项目 3.2", "value": 140 }
]
}
]
},
"network": {
"nodes": [
{ "id": "A", "group": 1 },
{ "id": "B", "group": 1 },
{ "id": "C", "group": 1 },
{ "id": "D", "group": 2 },
{ "id": "E", "group": 2 },
{ "id": "F", "group": 3 },
{ "id": "G", "group": 3 },
{ "id": "H", "group": 3 }
],
"links": [
{ "source": "A", "target": "B", "value": 1 },
{ "source": "A", "target": "C", "value": 2 },
{ "source": "B", "target": "C", "value": 1 },
{ "source": "C", "target": "D", "value": 3 },
{ "source": "D", "target": "E", "value": 2 },
{ "source": "E", "target": "F", "value": 1 },
{ "source": "F", "target": "G", "value": 2 },
{ "source": "F", "target": "H", "value": 1 },
{ "source": "G", "target": "H", "value": 1 }
]
},
"stackedData": [
{ "group": "Q1", "seriesA": 30, "seriesB": 40, "seriesC": 25 },
{ "group": "Q2", "seriesA": 45, "seriesB": 35, "seriesC": 30 },
{ "group": "Q3", "seriesA": 40, "seriesB": 50, "seriesC": 35 },
{ "group": "Q4", "seriesA": 55, "seriesB": 45, "seriesC": 40 }
],
"geographicPoints": [
{ "city": "伦敦", "latitude": 51.5074, "longitude": -0.1278, "value": 8900000 },
{ "city": "巴黎", "latitude": 48.8566, "longitude": 2.3522, "value": 2140000 },
{ "city": "柏林", "latitude": 52.5200, "longitude": 13.4050, "value": 3645000 },
{ "city": "马德里", "latitude": 40.4168, "longitude": -3.7038, "value": 3223000 },
{ "city": "罗马", "latitude": 41.9028, "longitude": 12.4964, "value": 2873000 }
],
"divergingData": [
{ "category": "项目 A", "value": -15 },
{ "category": "项目 B", "value": 8 },
{ "category": "项目 C", "value": -22 },
{ "category": "项目 D", "value": 18 },
{ "category": "项目 E", "value": -5 },
{ "category": "项目 F", "value": 25 },
{ "category": "项目 G", "value": -12 },
{ "category": "项目 H", "value": 14 }
]
}d3js-skill-cn
cn translation of project chrisvoncsefalvay/claude-d3js-skill
D3.js 颜色方案和调色板推荐
数据可视化中颜色选择的综合指南。
内置分类颜色方案
Category10(默认)
d3.schemeCategory10
// ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
// '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']特点:
- 10 种不同颜色
- 良好的色盲可访问性
- 大多数分类数据的默认选择
- 平衡的饱和度和亮度
用例: 一般用途的分类编码、图例项目、多个数据系列
Tableau10
d3.schemeTableau10特点:
- 10 种为数据可视化优化的颜色
- 专业外观
- 出色的区分度
用例: 商业仪表板、专业报告、演示文稿
Accent
d3.schemeAccent
// 8 种高饱和度颜色特点:
- 鲜亮、充满活力的颜色
- 高对比度
- 现代美学
用例: 突出重要类别、现代网络应用
Dark2
d3.schemeDark2
// 8 种较暗、柔和的颜色特点:
- 柔和的调色板
- 专业外观
- 适合深色背景
用例: 深色模式可视化、专业上下文
Paired
d3.schemePaired
// 12 种相似色调的配对特点:
- 浅色和深色变体的配对
- 适用于嵌套类别
- 12 种不同颜色
用例: 分组条形图、分层类别、前后比较
Pastel1 & Pastel2
d3.schemePastel1 // 9 种颜色
d3.schemePastel2 // 8 种颜色特点:
- 柔软、低饱和度颜色
- 温和外观
- 适合大面积
用例: 背景颜色、细微分类、舒缓可视化
Set1, Set2, Set3
d3.schemeSet1 // 9 种颜色 - 鲜艳
d3.schemeSet2 // 8 种颜色 - 柔和
d3.schemeSet3 // 12 种颜色 - 柔和特点:
- Set1:高饱和度,最大区分度
- Set2:专业,平衡
- Set3:细微,多种类别
用例: 根据视觉层次需求而定
序列颜色方案
序列方案使用单一色调或渐变将连续数据从低值映射到高值。
单色序列
蓝色:
d3.interpolateBlues
d3.schemeBlues[9] // 9 步离散版本其他单色选项:
d3.interpolateGreens/d3.schemeGreensd3.interpolateOranges/d3.schemeOrangesd3.interpolatePurples/d3.schemePurplesd3.interpolateReds/d3.schemeRedsd3.interpolateGreys/d3.schemeGreys
用例:
- 简单热力图
- 等值区域地图
- 密度图
- 单指标可视化
多色序列
Viridis(推荐):
d3.interpolateViridis特点:
- 感知均匀
- 色盲友好
- 适合打印
- 无视觉盲区
- 感知亮度单调递增
其他感知均匀选项:
d3.interpolatePlasma- 紫色到黄色d3.interpolateInferno- 黑色到白色,经过红/橙色d3.interpolateMagma- 黑色到白色,经过紫色d3.interpolateCividis- 色盲优化
色盲友好的:
d3.interpolateTurbo // 彩虹般但感知均匀
d3.interpolateCool // 青色到洋红色
d3.interpolateWarm // 橙色到黄色用例:
- 科学可视化
- 医学影像
- 任何高精度数据可视化
- 可访问的可视化
传统序列
黄-橙-红:
d3.interpolateYlOrRd
d3.schemeYlOrRd[9]黄-绿-蓝:
d3.interpolateYlGnBu
d3.schemeYlGnBu[9]其他多色:
d3.interpolateBuGn- 蓝色到绿色d3.interpolateBuPu- 蓝色到紫色d3.interpolateGnBu- 绿色到蓝色d3.interpolateOrRd- 橙色到红色d3.interpolatePuBu- 紫色到蓝色d3.interpolatePuBuGn- 紫色到蓝绿色d3.interpolatePuRd- 紫色到红色d3.interpolateRdPu- 红色到紫色d3.interpolateYlGn- 黄色到绿色d3.interpolateYlOrBr- 黄色到橙棕色
用例: 传统数据可视化、熟悉的颜色关联(温度、植被、水)
发散颜色方案
发散方案使用两种不同色调突出偏离中心值的情况。
红-蓝(温度)
d3.interpolateRdBu
d3.schemeRdBu[11]特点:
- 直观的温度比喻
- 强烈对比
- 清晰的正/负区分
用例: 温度、利润/损失、高于/低于平均值、相关性
红-黄-蓝
d3.interpolateRdYlBu
d3.schemeRdYlBu[11]特点:
- 三色渐变
- 通过黄色的柔和过渡
- 更多视觉步骤
用例: 当极端值需要强调且中间值需要可见时
其他发散方案
交通灯:
d3.interpolateRdYlGn // 红色(差)到绿色(好)光谱(彩虹):
d3.interpolateSpectral // 全光谱其他选项:
d3.interpolateBrBG- 棕色到蓝绿色d3.interpolatePiYG- 粉色到黄绿色d3.interpolatePRGn- 紫色到绿色d3.interpolatePuOr- 紫色到橙色d3.interpolateRdGy- 红色到灰色
用例: 根据语义含义和可访问性需求选择
色盲友好的调色板
一般准则
1. 避免红绿组合(最常见的色盲) 2. 使用蓝橙发散代替红绿 3. 添加纹理或图案作为冗余编码 4. 使用模拟工具测试
推荐的色盲安全方案
分类:
// Okabe-Ito 调色板(色盲安全)
const okabePalette = [
'#E69F00', // 橙色
'#56B4E9', // 天蓝色
'#009E73', // 蓝绿色
'#F0E442', // 黄色
'#0072B2', // 蓝色
'#D55E00', // 朱红色
'#CC79A7', // 红紫色
'#000000' // 黑色
];
const colourScale = d3.scaleOrdinal()
.domain(categories)
.range(okabePalette);序列:
// 使用 Viridis、Cividis 或蓝色
d3.interpolateViridis // 最佳整体
d3.interpolateCividis // 为色觉障碍优化
d3.interpolateBlues // 简单、安全发散:
// 使用蓝橙代替红绿
d3.interpolateBrBG
d3.interpolatePuOr自定义颜色调色板
创建自定义序列
const customSequential = d3.scaleLinear()
.domain([0, 100])
.range(['#e8f4f8', '#006d9c']) // 浅蓝色到深蓝色
.interpolate(d3.interpolateLab); // 感知均匀创建自定义发散
const customDiverging = d3.scaleLinear()
.domain([0, 50, 100])
.range(['#ca0020', '#f7f7f7', '#0571b0']) // 红色、灰色、蓝色
.interpolate(d3.interpolateLab);创建自定义分类
// 品牌颜色
const brandPalette = [
'#FF6B6B', // 主红色
'#4ECDC4', // 次青色
'#45B7D1', // 第三蓝色
'#FFA07A', // 强调珊瑚色
'#98D8C8' // 强调薄荷色
];
const colourScale = d3.scaleOrdinal()
.domain(categories)
.range(brandPalette);语义颜色关联
通用颜色含义
红色:
- 危险、错误、负面
- 高温
- 债务、损失
绿色:
- 成功、正面
- 增长、植被
- 利润、收益
蓝色:
- 信任、冷静
- 水、低温
- 信息、中性
黄色/橙色:
- 警告、注意
- 能量、温暖
- 注意
灰色:
- 中性、非活动
- 缺失数据
- 背景
上下文特定的调色板
金融:
const financialColours = {
profit: '#27ae60',
loss: '#e74c3c',
neutral: '#95a5a6',
highlight: '#3498db'
};温度:
const temperatureScale = d3.scaleSequential(d3.interpolateRdYlBu)
.domain([40, -10]); // 热到冷(反转)交通/状态:
const statusColours = {
success: '#27ae60',
warning: '#f39c12',
error: '#e74c3c',
info: '#3498db',
neutral: '#95a5a6'
};可访问性最佳实践
对比度比率
确保颜色与背景之间有足够的对比度:
// 良好对比度示例
const highContrast = {
background: '#ffffff',
text: '#2c3e50',
primary: '#3498db',
secondary: '#e74c3c'
};WCAG 指南:
- 普通文本:最小 4.5:1
- 大文本:最小 3:1
- UI 组件:最小 3:1
冗余编码
绝不要仅依赖颜色传达信息:
// 添加图案或形状
const symbols = ['circle', 'square', 'triangle', 'diamond'];
// 添加文本标签
// 使用线条样式(实线、虚线、点线)
// 使用大小编码测试
测试可视化中的色盲:
- Chrome 开发者工具(渲染 > 模拟视觉缺陷)
- Color Oracle(免费桌面应用程序)
- Coblis(在线模拟器)
专业颜色推荐
数据新闻
// Guardian 风格
const guardianPalette = [
'#005689', // Guardian 蓝色
'#c70000', // Guardian 红色
'#7d0068', // Guardian 粉色
'#951c75', // Guardian 紫色
];
// FT 风格
const ftPalette = [
'#0f5499', // FT 蓝色
'#990f3d', // FT 红色
'#593380', // FT 紫色
'#262a33', // FT 黑色
];学术/科学
// Nature 期刊风格
const naturePalette = [
'#0071b2', // 蓝色
'#d55e00', // 朱红色
'#009e73', // 绿色
'#f0e442', // 黄色
];
// 连续数据使用 Viridis
const scientificScale = d3.scaleSequential(d3.interpolateViridis);企业/商业
// 专业、保守
const corporatePalette = [
'#003f5c', // 深蓝色
'#58508d', // 紫色
'#bc5090', // 洋红色
'#ff6361', // 珊瑚色
'#ffa600' // 橙色
];动态颜色选择
基于数据范围
function selectColourScheme(data) {
const extent = d3.extent(data);
const hasNegative = extent[0] < 0;
const hasPositive = extent[1] > 0;
if (hasNegative && hasPositive) {
// 发散:数据跨越零
return d3.scaleSequentialSymlog(d3.interpolateRdBu)
.domain([extent[0], 0, extent[1]]);
} else {
// 序列:全部为正或全部为负
return d3.scaleSequential(d3.interpolateViridis)
.domain(extent);
}
}基于类别数量
function selectCategoricalScheme(categories) {
const n = categories.length;
if (n <= 10) {
return d3.scaleOrdinal(d3.schemeTableau10);
} else if (n <= 12) {
return d3.scaleOrdinal(d3.schemePaired);
} else {
// 对于许多类别,使用序列与量化
return d3.scaleQuantize()
.domain([0, n - 1])
.range(d3.quantize(d3.interpolateRainbow, n));
}
}应避免的常见颜色错误
1. 彩虹渐变为序列数据
- 问题:不感知均匀,难以阅读
- 解决方案:使用 Viridis、蓝色或其他均匀方案
2. 红绿用于发散(色盲)
- 问题:8% 的男性无法区分
- 解决方案:使用蓝橙或紫绿
3. 太多分类颜色
- 问题:难以区分和记住
- 解决方案:限制为 5-8 个类别,使用分组
4. 对比度不足
- 问题:可读性差
- 解决方案:测试对比度比率,使用深色在浅背景上
5. 文化不一致的颜色
- 问题:混淆语义含义
- 解决方案:研究目标受众的颜色关联
6. 反转的温度标度
- 问题:反直觉(红色 = 冷)
- 解决方案:红色/橙色 = 热,蓝色 = 冷
快速参考指南
需要显示...
- 类别(≤10):
d3.schemeCategory10或d3.schemeTableau10 - 类别(>10):
d3.schemePaired或分组类别 - 序列(通用):
d3.interpolateViridis - 序列(科学):
d3.interpolateViridis或d3.interpolatePlasma - 序列(温度):
d3.interpolateRdYlBu(反转) - 发散(零):
d3.interpolateRdBu或d3.interpolateBrBG - 发散(好/坏):
d3.interpolateRdYlGn(反转) - 色盲安全(分类): Okabe-Ito 调色板(如上所示)
- 色盲安全(序列):
d3.interpolateCividis或d3.interpolateBlues - 色盲安全(发散):
d3.interpolatePuOr或d3.interpolateBrBG
始终记住: 1. 测试色盲 2. 确保足够对比度 3. 适当使用语义颜色 4. 添加冗余编码(图案、标签) 5. 保持简洁(颜色越少 = 越清晰的可视化)
D3.js 可视化模式
此参考提供了常见 d3.js 可视化类型的详细代码模式。
层次可视化
树状图
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const tree = d3.tree().size([height - 100, width - 200]);
const root = d3.hierarchy(data);
tree(root);
const g = svg.append("g")
.attr("transform", "translate(100,50)");
// 链接
g.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x))
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-width", 2);
// 节点
const node = g.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("circle")
.attr("r", 6)
.attr("fill", d => d.children ? "#555" : "#999");
node.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.children ? -8 : 8)
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name)
.style("font-size", "12px");
}, [data]);树状地图
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
d3.treemap()
.size([width, height])
.padding(2)
.round(true)(root);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const cell = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);
cell.append("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => colourScale(d.parent.data.name))
.attr("stroke", "white")
.attr("stroke-width", 2);
cell.append("text")
.attr("x", 4)
.attr("y", 16)
.text(d => d.data.name)
.style("font-size", "12px")
.style("fill", "white");
}, [data]);日照图
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const radius = Math.min(width, height) / 2;
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
const partition = d3.partition()
.size([2 * Math.PI, radius]);
partition(root);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
g.selectAll("path")
.data(root.descendants())
.join("path")
.attr("d", arc)
.attr("fill", d => colourScale(d.depth))
.attr("stroke", "white")
.attr("stroke-width", 1);
}, [data]);弦图
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");
}
// 数据格式示例:
// const data = [
// { source: 'Category A', target: 'Category B', value: 100 },
// { source: 'Category A', target: 'Category C', value: 50 },
// { source: 'Category B', target: 'Category C', value: 75 }
// ];
// drawChordDiagram(data);高级图表类型
热力图
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 data = [
// { row: 'Monday', column: 'Morning', value: 42 },
// { row: 'Monday', column: 'Afternoon', value: 78 },
// { row: 'Tuesday', column: 'Morning', value: 65 },
// { row: 'Tuesday', column: 'Afternoon', value: 55 }
// ];
// drawHeatmap(data);带渐变的面积图
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
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 defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "areaGradient")
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "100%");
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "steelblue")
.attr("stop-opacity", 0.8);
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "steelblue")
.attr("stop-opacity", 0.1);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([innerHeight, 0]);
const area = d3.area()
.x(d => xScale(d.date))
.y0(innerHeight)
.y1(d => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "url(#areaGradient)")
.attr("d", area);
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.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);堆叠条形图
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
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 categories = Object.keys(data[0]).filter(k => k !== 'group');
const stackedData = d3.stack().keys(categories)(data);
const xScale = d3.scaleBand()
.domain(data.map(d => d.group))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])])
.range([innerHeight, 0]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
g.selectAll("g")
.data(stackedData)
.join("g")
.attr("fill", (d, i) => colourScale(i))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => xScale(d.data.group))
.attr("y", d => yScale(d[1]))
.attr("height", d => yScale(d[0]) - yScale(d[1]))
.attr("width", xScale.bandwidth());
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);分组条形图
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
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 categories = Object.keys(data[0]).filter(k => k !== 'group');
const x0Scale = d3.scaleBand()
.domain(data.map(d => d.group))
.range([0, innerWidth])
.padding(0.1);
const x1Scale = d3.scaleBand()
.domain(categories)
.range([0, x0Scale.bandwidth()])
.padding(0.05);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => Math.max(...categories.map(c => d[c])))])
.range([innerHeight, 0]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const group = g.selectAll("g")
.data(data)
.join("g")
.attr("transform", d => `translate(${x0Scale(d.group)},0)`);
group.selectAll("rect")
.data(d => categories.map(key => ({ key, value: d[key] })))
.join("rect")
.attr("x", d => x1Scale(d.key))
.attr("y", d => yScale(d.value))
.attr("width", x1Scale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", d => colourScale(d.key));
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x0Scale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);气泡图
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
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)])
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0]);
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.size)])
.range([0, 50]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
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.6)
.attr("stroke", "white")
.attr("stroke-width", 2);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);地理可视化
带点的基本地图
useEffect(() => {
if (!geoData || !pointData) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const projection = d3.geoMercator()
.fitSize([width, height], geoData);
const pathGenerator = d3.geoPath().projection(projection);
// 绘制地图
svg.selectAll("path")
.data(geoData.features)
.join("path")
.attr("d", pathGenerator)
.attr("fill", "#e0e0e0")
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
// 绘制点
svg.selectAll("circle")
.data(pointData)
.join("circle")
.attr("cx", d => projection([d.longitude, d.latitude])[0])
.attr("cy", d => projection([d.longitude, d.latitude])[1])
.attr("r", 5)
.attr("fill", "steelblue")
.attr("opacity", 0.7);
}, [geoData, pointData]);等值区域地图
useEffect(() => {
if (!geoData || !valueData) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const projection = d3.geoMercator()
.fitSize([width, height], geoData);
const pathGenerator = d3.geoPath().projection(projection);
// 创建值查找
const valueLookup = new Map(valueData.map(d => [d.id, d.value]));
// 颜色比例尺
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(valueData, d => d.value)]);
svg.selectAll("path")
.data(geoData.features)
.join("path")
.attr("d", pathGenerator)
.attr("fill", d => {
const value = valueLookup.get(d.id);
return value ? colourScale(value) : "#e0e0e0";
})
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
}, [geoData, valueData]);高级交互
刷选和缩放
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
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 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]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const circles = 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");
// 添加刷选
const brush = d3.brush()
.extent([[0, 0], [innerWidth, innerHeight]])
.on("start brush", (event) => {
if (!event.selection) return;
const [[x0, y0], [x1, y1]] = event.selection;
circles.attr("fill", d => {
const cx = xScale(d.x);
const cy = yScale(d.y);
return (cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1)
? "orange"
: "steelblue";
});
});
g.append("g")
.attr("class", "brush")
.call(brush);
}, [data]);图表间的链接刷选
function LinkedCharts({ data }) {
const [selectedPoints, setSelectedPoints] = useState(new Set());
const svg1Ref = useRef();
const svg2Ref = useRef();
useEffect(() => {
// 图表 1:散点图
const svg1 = d3.select(svg1Ref.current);
svg1.selectAll("*").remove();
// ... 创建第一个图表 ...
const circles1 = svg1.selectAll("circle")
.data(data)
.join("circle")
.attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue");
// 图表 2:条形图
const svg2 = d3.select(svg2Ref.current);
svg2.selectAll("*").remove();
// ... 创建第二个图表 ...
const bars = svg2.selectAll("rect")
.data(data)
.join("rect")
.attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue");
// 为第一个图表添加刷选
const brush = d3.brush()
.on("start brush end", (event) => {
if (!event.selection) {
setSelectedPoints(new Set());
return;
}
const [[x0, y0], [x1, y1]] = event.selection;
const selected = new Set();
data.forEach(d => {
const x = xScale(d.x);
const y = yScale(d.y);
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
selected.add(d.id);
}
});
setSelectedPoints(selected);
});
svg1.append("g").call(brush);
}, [data, selectedPoints]);
return (
<div>
<svg ref={svg1Ref} width="400" height="300" />
<svg ref={svg2Ref} width="400" height="300" />
</div>
);
}动画模式
进入、更新、退出与过渡
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
const circles = svg.selectAll("circle")
.data(data, d => d.id); // 键函数用于对象恒定性
// 退出:删除旧元素
circles.exit()
.transition()
.duration(500)
.attr("r", 0)
.remove();
// 更新:修改现有元素
circles
.transition()
.duration(500)
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("fill", "steelblue");
// 进入:添加新元素
circles.enter()
.append("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 0)
.attr("fill", "steelblue")
.transition()
.duration(500)
.attr("r", 5);
}, [data]);路径变形
useEffect(() => {
if (!data1 || !data2) return;
const svg = d3.select(svgRef.current);
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX);
const path = svg.select("path");
// 从数据1变形到数据2
path
.datum(data1)
.attr("d", line)
.transition()
.duration(1000)
.attrTween("d", function() {
const previous = d3.select(this).attr("d");
const current = line(data2);
return d3.interpolatePath(previous, current);
});
}, [data1, data2]);D3.js 比例尺参考
所有 d3 比例尺类型的综合指南,包含示例和用例。
连续比例尺
线性比例尺
将连续输入域映射到连续输出范围,使用线性插值。
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
scale(50); // 返回 250
scale(0); // 返回 0
scale(100); // 返回 500
// 反向比例尺(从输出获取输入)
scale.invert(250); // 返回 50用例:
- 定量数据最常见的比例尺
- 轴、条形长度、位置编码
- 温度、价格、计数、测量值
方法:
.domain([min, max])- 设置输入域.range([min, max])- 设置输出范围.invert(value)- 从范围值获取域值.clamp(true)- 将输出限制在范围边界内.nice()- 扩展域到合适的圆整值
幂比例尺
将连续输入映射到连续输出,使用指数变换。
const sqrtScale = d3.scalePow()
.exponent(0.5) // 平方根
.domain([0, 100])
.range([0, 500]);
const squareScale = d3.scalePow()
.exponent(2) // 平方
.domain([0, 100])
.range([0, 500]);
// 平方根的简写
const sqrtScale2 = d3.scaleSqrt()
.domain([0, 100])
.range([0, 500]);用例:
- 感知缩放(人类感知是非线性的)
- 面积编码(使用平方根将值映射到圆半径)
- 强调小值或大值的差异
对数比例尺
将连续输入映射到连续输出,使用对数变换。
const logScale = d3.scaleLog()
.domain([1, 1000]) // 必须为正数
.range([0, 500]);
logScale(1); // 返回 0
logScale(10); // 返回 ~167
logScale(100); // 返回 ~333
logScale(1000); // 返回 500用例:
- 跨越多个数量级的数据
- 人口、GDP、财富分布
- 对数轴
- 指数增长可视化
重要: 域值必须严格为正(>0)。
时间比例尺
专门用于时间数据的线性比例尺。
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 800]);
timeScale(new Date(2022, 0, 1)); // 返回 400
// 反向获取日期
timeScale.invert(400); // 返回 2022 年中期的日期对象用例:
- 时间序列可视化
- 时间轴
- 时间动画
- 基于日期的交互
方法:
.nice()- 扩展域到合适的时间间隔.ticks(count)- 生成间距合适的刻度值- 所有线性比例尺方法都适用
量化比例尺
将连续输入映射到离散输出桶。
const quantizeScale = d3.scaleQuantize()
.domain([0, 100])
.range(['低', '中', '高']);
quantizeScale(25); // 返回 '低'
quantizeScale(50); // 返回 '中'
quantizeScale(75); // 返回 '高'
// 获取阈值
quantizeScale.thresholds(); // 返回 [33.33, 66.67]用例:
- 对连续数据分箱
- 热力图颜色
- 风险类别(低/中/高)
- 年龄组、收入等级
分位数比例尺
基于分位数将连续输入映射到离散输出。
const quantileScale = d3.scaleQuantile()
.domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // 示例数据
.range(['低', '中', '高']);
quantileScale(8); // 基于分位数位置返回
quantileScale.quantiles(); // 返回分位数阈值用例:
- 无论分布如何的等大小组
- 百分位数分类
- 处理偏斜分布
阈值比例尺
使用自定义阈值将连续输入映射到离散输出。
const thresholdScale = d3.scaleThreshold()
.domain([0, 10, 20])
.range(['冰点', '冷', '暖', '热']);
thresholdScale(-5); // 返回 '冰点'
thresholdScale(5); // 返回 '冷'
thresholdScale(15); // 返回 '暖'
thresholdScale(25); // 返回 '热'用例:
- 自定义断点
- 等级边界(A、B、C、D、F)
- 温度类别
- 空气质量指数
序列比例尺
序列颜色比例尺
将连续输入映射到连续颜色渐变。
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
colourScale(0); // 返回最浅蓝色
colourScale(50); // 返回中间蓝色
colourScale(100); // 返回最深蓝色可用插值器:
单色:
d3.interpolateBlues,d3.interpolateGreens,d3.interpolateRedsd3.interpolateOranges,d3.interpolatePurples,d3.interpolateGreys
多色:
d3.interpolateViridis,d3.interpolateInferno,d3.interpolateMagmad3.interpolatePlasma,d3.interpolateWarm,d3.interpolateCoold3.interpolateCubehelixDefault,d3.interpolateTurbo
用例:
- 热力图、等值区域地图
- 连续数据可视化
- 温度、海拔、密度
发散颜色比例尺
将连续输入映射到带有中点的发散颜色渐变。
const divergingScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
divergingScale(-10); // 返回红色
divergingScale(0); // 返回白色/中性
divergingScale(10); // 返回蓝色可用插值器:
d3.interpolateRdBu- 红色到蓝色d3.interpolateRdYlBu- 红色、黄色、蓝色d3.interpolateRdYlGn- 红色、黄色、绿色d3.interpolatePiYG- 粉色、黄色、绿色d3.interpolateBrBG- 棕色、蓝绿色d3.interpolatePRGn- 紫色、绿色d3.interpolatePuOr- 紫色、橙色d3.interpolateRdGy- 红色、灰色d3.interpolateSpectral- 彩虹光谱
用例:
- 具有意义中点的数据(零、平均值、中性)
- 正/负值
- 以上/以下比较
- 相关矩阵
序列分位数比例尺
结合序列颜色与分位数映射。
const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues)
.domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]);
// 基于分位数位置映射用例:
- 感知均匀分箱
- 处理异常值
- 偏斜分布
序数比例尺
带比例尺
将离散输入映射到连续带(矩形),可选填充。
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
bandScale('A'); // 返回起始位置(例如,0)
bandScale('B'); // 返回起始位置(例如,110)
bandScale.bandwidth(); // 返回每个带的宽度(例如,95)
bandScale.step(); // 返回总步长,包括填充
bandScale.paddingInner(); // 返回内部填充(带之间)
bandScale.paddingOuter(); // 返回外部填充(边缘处)用例:
- 条形图(最常见的用例)
- 分组元素
- 分类轴
- 热力图单元格
填充选项:
.padding(value)- 设置内部和外部填充(0-1).paddingInner(value)- 带之间的填充(0-1).paddingOuter(value)- 边缘处的填充(0-1).align(value)- 带的对齐(0-1,默认 0.5)
点比例尺
将离散输入映射到连续点(无宽度)。
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.5);
pointScale('A'); // 返回位置(例如,50)
pointScale('B'); // 返回位置(例如,150)
pointScale('C'); // 返回位置(例如,250)
pointScale('D'); // 返回位置(例如,350)
pointScale.step(); // 返回点之间的距离用例:
- 折线图分类 x 轴
- 带有分类轴的散点图
- 网络图中的节点位置
- 类别的任何点定位
序数颜色比例尺
将离散输入映射到离散输出(颜色、形状等)。
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
colourScale('apples'); // 返回第一种颜色
colourScale('oranges'); // 返回第二种颜色
colourScale('apples'); // 返回相同的第一个颜色(一致)
// 自定义范围
const customScale = d3.scaleOrdinal()
.domain(['cat1', 'cat2', 'cat3'])
.range(['#FF6B6B', '#4ECDC4', '#45B7D1']);内置颜色方案:
分类:
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 种颜色
用例:
- 类别颜色
- 图例项目
- 多系列图表
- 网络节点类型
比例尺工具
合适的域
扩展域到合适的圆整值。
const scale = d3.scaleLinear()
.domain([0.201, 0.996])
.nice();
scale.domain(); // 返回 [0.2, 1.0]
// 带计数(近似刻度计数)
const scale2 = d3.scaleLinear()
.domain([0.201, 0.996])
.nice(5);限制
将输出限制在范围边界内。
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500])
.clamp(true);
scale(-10); // 返回 0(限制)
scale(150); // 返回 500(限制)复制比例尺
创建独立副本。
const scale1 = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
const scale2 = scale1.copy();
// scale2 独立于 scale1刻度生成
为轴生成合适的刻度值。
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
scale.ticks(10); // 生成 ~10 个刻度
scale.tickFormat(10); // 获取刻度的格式函数
scale.tickFormat(10, ".2f"); // 自定义格式(2 个小数位)
// 时间比例尺刻度
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]);
timeScale.ticks(d3.timeYear); // 年度刻度
timeScale.ticks(d3.timeMonth, 3); // 每 3 个月
timeScale.tickFormat(5, "%Y-%m"); // 格式化为年-月颜色空间和插值
RGB 插值
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"]);
// 默认:RGB 插值HSL 插值
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateHsl);
// 更平滑的颜色过渡Lab 插值
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateLab);
// 感知均匀HCL 插值
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateHcl);
// 感知均匀,带色相常见模式
带自定义中点的发散比例尺
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]);
// 与圆圈一起使用
circle.attr("r", d => radiusScale(d.value));基于数据范围的自适应比例尺
function createAdaptiveScale(data) {
const extent = d3.extent(data);
const range = extent[1] - extent[0];
// 如果数据跨越 >2 个数量级,则使用对数比例尺
if (extent[1] / extent[0] > 100) {
return d3.scaleLog()
.domain(extent)
.range([0, width]);
}
// 否则使用线性
return d3.scaleLinear()
.domain(extent)
.range([0, width]);
}显式类别的颜色比例尺
const colourScale = d3.scaleOrdinal()
.domain(['Low Risk', 'Medium Risk', 'High Risk'])
.range(['#2ecc71', '#f39c12', '#e74c3c'])
.unknown('#95a5a6'); // 未知值的后备