如何使用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
相关产品推荐
相关产品推荐

