使用Chart.js实现国家-城市双层嵌套环形图的问题咨询
Chart.js 实现国家-城市双层嵌套环形图方案
你现有代码有5个直接导致效果异常的问题:
- 外层城市数据统计逻辑错误:仅统计了每个国家下去重后的城市种类数,没有统计每个城市的实际出现次数,且外层数据集长度和内层逻辑不匹配,无法拆分扇区
- 两个环形数据集未做半径/宽度区分,会完全重叠
- 配置项
circumference: 90会让图表只渲染四分之一圈,无法显示完整环形 - 颜色随机生成,没有同国家色系关联,无法体现内外层扇区的归属关系
- 预处理数据存在拼写错误:
China被拼成Chine,Budapest被拼成Budapes,会导致同主体数据被错误拆分
正确实现步骤
1. 数据处理逻辑修正
核心原则:外层所有属于同一个国家的城市数值之和,必须等于内层对应国家的数值,Chart.js会自动按数值比例分配角度,实现内外层扇区严丝合缝的对应效果。
// 原始输入数据 const rawData = [ {city: "Budapest", country: "Hungary"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Istanbul", country: "Turkey"}, {city: "Ho Chi Minh", country: "Vietnam"}, {city: "Shenzen", country: "China"}, {city: "Budapes", country: "Hungary"}, {city: "Budapest", country: "Hungary"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Istanbul", country: "Turkey"}, {city: "Budapest", country: "Hungary"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Shenzen", country: "China"}, {city: "Istanbul", country: "Turkey"}, ] // 按国家分组,同步统计每个国家下各城市的出现次数 const countryGroup = {} rawData.forEach(item => { // 可在此处加名称标准化逻辑,修正拼写错误 const countryName = item.country === 'Chine' ? 'China' : item.country const cityName = item.city === 'Budapes' ? 'Budapest' : item.city if (!countryGroup[countryName]) countryGroup[countryName] = {} if (!countryGroup[countryName][cityName]) countryGroup[countryName][cityName] = 0 countryGroup[countryName][cityName] += 1 }) // 生成内层(国家层)数据 const innerLabels = Object.keys(countryGroup) const innerData = innerLabels.map(country => { return Object.values(countryGroup[country]).reduce((sum, count) => sum + count, 0) }) // 内层配置基础主题色 const innerColors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'] // 生成外层(城市层)数据,顺序严格和内层国家对齐 const outerLabels = [] const outerData = [] const outerColors = [] innerLabels.forEach((country, countryIndex) => { const cityList = countryGroup[country] Object.entries(cityList).forEach(([city, count]) => { outerLabels.push(city) outerData.push(count) // 外层用同国家基础色的浅色调,体现归属关系,不想引入颜色库可以手动配置同色系色值 outerColors.push(lightenColor(innerColors[countryIndex], 20)) }) }) // 简易颜色明度调整函数,不需要依赖第三方库 function lightenColor(hex, percent) { const num = parseInt(hex.replace('#',''),16) const amt = Math.round(2.55 * percent) const R = Math.min(255, (num >> 16) + amt) const G = Math.min(255, (num >> 8 & 0x00FF) + amt) const B = Math.min(255, (num & 0x0000FF) + amt) return `#${(0x1000000 + R*0x10000 + G*0x100 + B).toString(16).slice(1)}` }
2. 图表配置修正
this.locations_pie_data = { labels: [...innerLabels, ...outerLabels], datasets: [ // 外层:城市数据,环形更靠外 { type: 'doughnut', data: outerData, backgroundColor: outerColors, weight: 1, borderWidth: 1, borderColor: '#fff' }, // 内层:国家数据,环形更靠内 { type: 'doughnut', data: innerData, backgroundColor: innerColors, weight: 1, borderWidth: 1, borderColor: '#fff' } ], options: { cutout: '40%', // 控制中心空心区域大小 plugins: { legend: { position: 'right', // 过滤图例,只显示国家层级,避免城市太多导致图例杂乱 filter: (item) => innerLabels.includes(item.text) }, tooltip: { callbacks: { label: (ctx) => { const total = ctx.dataset.data.reduce((a,b) => a+b, 0) const percentage = ((ctx.parsed / total)*100).toFixed(1) return `${ctx.label}: ${ctx.parsed} (${percentage}%)` } } } } } }
最终效果
- 内层4个扇区分别对应4个国家,扇区大小和该国总数据量成正比
- 外层每个国家对应的扇区自动拆分为下属城市的小扇区,大小和城市出现次数成正比,同国家的外层扇区用同色系浅色,和内层扇区视觉关联
- 鼠标悬浮可查看对应分类的数值和占比
- 图例仅展示国家分类,信息层级清晰

内容的提问来源于stack exchange,提问作者Daniel H.
相关产品推荐
相关产品推荐

