D3.js 布局算法 #
布局算法是 D3.js 提供的高级工具,用于自动计算复杂图形的位置和形状,使创建网络图、树图、饼图等变得简单。
布局概述 #
核心概念 #
text
┌─────────────────────────────────────────────────────────────┐
│ 布局工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原始数据 ──► 布局算法 ──► 计算结果 ──► 渲染图形 │
│ │
│ {nodes, force() {x, y, ...} <circle cx=x> │
│ links} │
│ │
└─────────────────────────────────────────────────────────────┘
布局类型 #
| 布局类型 | 描述 | 用途 |
|---|---|---|
| d3-force | 力导向布局 | 网络关系图 |
| d3-hierarchy | 层次布局 | 树图、簇图 |
| d3-pie | 饼图布局 | 饼图、环形图 |
| d3-chord | 和弦图布局 | 关系矩阵 |
| d3-sankey | 桑基图布局 | 流向图 |
力导向图 d3-force #
基本用法 #
javascript
const nodes = [
{ id: 'A' },
{ id: 'B' },
{ id: 'C' }
];
const links = [
{ source: 'A', target: 'B' },
{ source: 'B', target: 'C' },
{ source: 'C', target: 'A' }
];
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked);
function ticked() {
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);
}
const link = svg.append('g')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', '#999');
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 5)
.attr('fill', 'steelblue');
力的类型 #
forceCenter - 中心力 #
javascript
d3.forceCenter(x, y)
simulation.force('center', d3.forceCenter(width / 2, height / 2));
forceManyBody - 多体力 #
javascript
d3.forceManyBody()
simulation.force('charge', d3.forceManyBody()
.strength(-100)
.distanceMin(1)
.distanceMax(1000)
);
forceLink - 连接力 #
javascript
d3.forceLink(links)
simulation.force('link', d3.forceLink(links)
.id(d => d.id)
.distance(30)
.strength(1)
);
forceCollide - 碰撞力 #
javascript
d3.forceCollide(radius)
simulation.force('collide', d3.forceCollide()
.radius(d => d.r + 5)
.strength(1)
);
forceX / forceY - 定位力 #
javascript
d3.forceX(x)
d3.forceY(y)
simulation
.force('x', d3.forceX(width / 2).strength(0.1))
.force('y', d3.forceY(height / 2).strength(0.1));
forceRadial - 径向力 #
javascript
d3.forceRadial(radius, x, y)
simulation.force('radial', d3.forceRadial(100, width / 2, height / 2));
控制模拟 #
javascript
simulation.stop();
simulation.restart();
simulation.tick(10);
simulation.alpha(1);
simulation.alphaMin(0.001);
simulation.alphaDecay(0.02);
simulation.velocityDecay(0.4);
拖拽交互 #
javascript
const drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
node.call(drag);
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
层次布局 d3-hierarchy #
创建层次结构 #
javascript
const data = {
name: 'Root',
children: [
{
name: 'A',
children: [
{ name: 'A1' },
{ name: 'A2' }
]
},
{
name: 'B',
children: [
{ name: 'B1' },
{ name: 'B2' }
]
}
]
};
const root = d3.hierarchy(data);
层次结构属性 #
javascript
root.data
root.depth
root.height
root.parent
root.children
root.descendants()
root.ancestors()
root.leaves()
root.links()
root.path(node)
树图布局 d3.tree #
javascript
const treeLayout = d3.tree()
.size([height, width]);
treeLayout(root);
const link = svg.selectAll('.link')
.data(root.links())
.enter()
.append('path')
.attr('class', 'link')
.attr('d', d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x)
);
const node = svg.selectAll('.node')
.data(root.descendants())
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.y},${d.x})`);
node.append('circle')
.attr('r', 4);
node.append('text')
.attr('dy', 3)
.attr('x', d => d.children ? -8 : 8)
.style('text-anchor', d => d.children ? 'end' : 'start')
.text(d => d.data.name);
簇图布局 d3.cluster #
javascript
const clusterLayout = d3.cluster()
.size([height, width]);
clusterLayout(root);
径向树图 #
javascript
const treeLayout = d3.tree()
.size([2 * Math.PI, radius]);
const link = svg.selectAll('.link')
.data(root.links())
.enter()
.append('path')
.attr('d', d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y)
);
const node = svg.selectAll('.node')
.data(root.descendants())
.enter()
.append('g')
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
矩形树图 d3.treemap #
javascript
const treemap = d3.treemap()
.size([width, height])
.padding(1);
root.sum(d => d.value);
treemap(root);
const leaf = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x0},${d.y0})`);
leaf.append('rect')
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => colorScale(d.data.name));
leaf.append('text')
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
.enter()
.append('tspan')
.attr('x', 3)
.attr('y', (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`)
.text(d => d);
分区图 d3.partition #
javascript
const partition = d3.partition()
.size([width, height]);
root.sum(d => d.value);
partition(root);
打包图 d3.pack #
javascript
const pack = d3.pack()
.size([width, height])
.padding(3);
root.sum(d => d.value);
pack(root);
const node = svg.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x},${d.y})`);
node.append('circle')
.attr('r', d => d.r)
.attr('fill', d => d.children ? '#ddd' : 'steelblue');
旭日图 #
javascript
const partition = d3.partition()
.size([2 * Math.PI, radius]);
root.sum(d => d.value);
partition(root);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1);
svg.selectAll('path')
.data(root.descendants())
.enter()
.append('path')
.attr('d', arc)
.attr('fill', d => colorScale(d.depth));
饼图布局 d3.pie #
基本用法 #
javascript
const data = [10, 20, 30, 40, 50];
const pie = d3.pie()
.value(d => d)
.sort(null);
const pieData = pie(data);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
svg.selectAll('path')
.data(pieData)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => d3.schemeCategory10[i]);
饼图配置 #
javascript
const pie = d3.pie()
.value(d => d.value)
.sort((a, b) => a.value - b.value)
.startAngle(0)
.endAngle(2 * Math.PI)
.padAngle(0.02);
和弦图 d3.chord #
基本用法 #
javascript
const matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const chords = chord(matrix);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.radius(innerRadius);
svg.datum(chords)
.append('g')
.selectAll('g')
.data(d => d.groups)
.enter()
.append('g')
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => d3.schemeCategory10[i]);
svg.datum(chords)
.append('g')
.selectAll('path')
.data(d => d)
.enter()
.append('path')
.attr('d', ribbon)
.attr('fill', d => d3.schemeCategory10[d.source.index]);
力导向图完整示例 #
javascript
const width = 600;
const height = 400;
const nodes = [
{ id: 'A', group: 1 },
{ id: 'B', group: 1 },
{ id: 'C', group: 2 },
{ id: 'D', group: 2 },
{ id: 'E', group: 3 }
];
const links = [
{ source: 'A', target: 'B', value: 1 },
{ source: 'B', target: 'C', value: 2 },
{ source: 'C', target: 'D', value: 3 },
{ source: 'D', target: 'E', value: 1 },
{ source: 'E', target: 'A', value: 2 }
];
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(50))
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2));
const link = svg.append('g')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => Math.sqrt(d.value))
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6);
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 8)
.attr('fill', d => color(d.group))
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
node.append('title')
.text(d => d.id);
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;
}
布局方法速查表 #
| 布局 | 方法 | 描述 |
|---|---|---|
| force | forceSimulation | 创建力模拟 |
| force | force | 添加力 |
| force | on | 监听 tick 事件 |
| tree | d3.tree | 创建树布局 |
| tree | size | 设置大小 |
| cluster | d3.cluster | 创建簇布局 |
| treemap | d3.treemap | 创建矩形树图 |
| partition | d3.partition | 创建分区图 |
| pack | d3.pack | 创建打包图 |
| pie | d3.pie | 创建饼图布局 |
| chord | d3.chord | 创建和弦布局 |
下一步 #
现在你已经掌握了布局算法,接下来学习 常见图表,了解如何实现各种类型的图表!
最后更新:2026-03-28