You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用D3实现如图所示的多层饼图

多层饼图效果示例

D3 多层同心饼图实现方案

核心实现思路

这个效果属于嵌套环形饼图,核心是把不同层级的分类数据映射到不同半径的同心圆环上,保证同分支的外层子扇区总角度,和内层对应父扇区的角度完全对齐。
核心逻辑拆分:

  • 先把原始数据处理成树状层级结构,自动汇总每个父节点的数值,避免手动计算占比导致的内外层扇区错位
  • 以SVG画布中心为圆心,给每一层环配置独立的内外半径,层与层之间留固定间隙
  • 同个父分类下的子扇区用同色系不同明度做区分,扇区之间加白色描边做分割,还原示例视觉效果
  • 可按需增加hover高亮、中心信息提示、扇区标签等交互

开发步骤与代码逻辑

以下基于D3 v7版本实现,兼容大部分现代浏览器。

1. 画布初始化

先创建SVG画布,把绘图容器平移到画布正中心,后续所有扇区都以这个点为圆心绘制:

import * as d3 from 'd3';

// 画布尺寸配置
const config = {
  width: 800,
  height: 800,
  ringGap: 20 // 层与层之间的间隙
}
const svg = d3.select('#chart')
  .append('svg')
  .attr('width', config.width)
  .attr('height', config.height);
// 核心绘图组,平移到画布中心
const chartG = svg.append('g')
  .attr('transform', `translate(${config.width/2}, ${config.height/2})`);

2. 布局与数据预处理

配置每一层环的半径,用D3内置的层级处理工具自动计算节点值,为每一层生成对应的饼图布局和弧生成器:

// 3层环配置,从内到外设置半径,可按需增减层数
const ringConfs = [
  { innerR: 0, outerR: 100 }, // 最内层为实心圆,需要空心的话把innerR改成大于0的值即可
  { innerR: 120, outerR: 220 },
  { innerR: 240, outerR: 340 }
];
// 分类颜色比例尺
const colorScale = d3.scaleOrdinal(d3.schemeSet3);

// 示例层级数据,最末级节点带实际value值
const sourceData = {
  name: '总数据',
  children: [
    { name: '分类A', children: [{name: 'A-1', value: 120}, {name: 'A-2', value: 180}] },
    { name: '分类B', children: [{name: 'B-1', value: 200}, {name: 'B-2', value: 150}, {name: 'B-3', value: 100}] },
    { name: '分类C', children: [{name: 'C-1', value: 100}, {name: 'C-2', value: 150}] }
  ]
};

// 处理层级数据,自动汇总父节点value
const root = d3.hierarchy(sourceData).sum(d => d.value || 0);
// 提取每一层的节点数据(第0层是根节点,不需要绘制)
const layerNodes = [];
for(let depth = 1; depth <= ringConfs.length; depth++){
  layerNodes.push(root.descendants().filter(node => node.depth === depth));
}

// 初始化饼图布局,关闭自动排序保证节点顺序和数据一致
const pieLayout = d3.pie().value(d => d.value).sort(null);
// 为每一层生成弧生成器
const arcGenerators = ringConfs.map(conf => {
  return d3.arc().innerRadius(conf.innerR).outerRadius(conf.outerR);
});

3. 分层绘制扇区

从内到外遍历每一层数据,生成对应的扇区路径,绑定样式和交互:

// 遍历绘制每一层
layerNodes.forEach((nodes, layerIndex) => {
  const pieData = pieLayout(nodes);
  // 绘制扇区
  chartG.selectAll(`.ring-layer-${layerIndex}`)
    .data(pieData)
    .join('path')
    .attr('class', `ring-layer-${layerIndex}`)
    .attr('d', arcGenerators[layerIndex])
    .attr('fill', d => {
      // 最内层用基础分类色,外层基于父类颜色调整明度,形成同色系层级
      if(layerIndex === 0) return colorScale(d.data.name);
      const baseColor = d3.color(colorScale(d.data.parent.data.name));
      return baseColor.brighter(layerIndex * 0.3);
    })
    .attr('stroke', '#ffffff')
    .attr('stroke-width', 2)
    // hover交互
    .on('mouseenter', function(event, d){
      d3.select(this).attr('opacity', 0.7);
      chartG.select('.center-label').text(`${d.data.name}: ${d.value}`);
    })
    .on('mouseleave', function(){
      d3.select(this).attr('opacity', 1);
      chartG.select('.center-label').text('数据总览');
    });
});

// 绘制中心提示文字
chartG.append('text')
  .attr('class', 'center-label')
  .attr('text-anchor', 'middle')
  .attr('font-size', 22)
  .text('数据总览');

自定义配置方法

  • 层数调整:直接增减ringConfs数组的配置项即可,注意相邻环的半径要留出固定间隙,避免重叠
  • 样式修改:调整fill属性的逻辑可以自定义配色,给弧生成器加cornerRadius配置可以给扇区加圆角,修改stroke属性调整扇区间隔线的样式
  • 标签添加:需要显示扇区标签时,用arc.centroid()计算每个扇区的中心坐标,追加text元素即可,标签密集时可以加引线做外溢偏移
  • 数据适配:如果原始数据是扁平结构,只需要提前转成带children字段的树状结构传入即可,d3.hierarchy会自动完成父节点值汇总

常见踩坑:不要给每一层单独计算数值占比,否则会出现内外层扇区角度错位的问题,必须基于层级结构的汇总值计算饼图角度,才能保证同分支的父子扇区完全对齐。

内容的提问来源于stack exchange,提问作者Anuj Shaan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 02:42:18