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)
);
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