用Echarts手把手教你绘制炫酷旭日图附完整代码与避坑指南最近在做一个项目复盘的数据看板产品经理指着设计稿上那个层层嵌套、色彩绚丽的环形图问我“这个能实现吗要能点击钻取数据是动态的。”我一看这不就是旭日图嘛。说实话第一次接触旭日图时我也被它那复杂的层级结构和数据映射关系绕得头晕官方文档看几遍还是云里雾里更别提那些藏在配置项里的“坑”了。但折腾了几个项目后我发现只要摸清了它的“脾气”用Echarts实现一个既美观又实用的旭日图其实比想象中简单得多。这篇文章我就把自己从踩坑到熟练的实战经验整理出来面向那些已经熟悉Echarts基础、但想在复杂层级可视化上更进一步的前端开发者和数据分析师。我们不会停留在简单的API介绍而是直接切入数据格式的坑怎么绕、交互事件如何绑定、性能优化点在哪里这些实际开发中必然遇到的问题。我会提供能直接复制粘贴、并根据你的数据稍作修改就能运行的完整代码同时对比在Python生态里常用的Pyecharts方案帮你根据技术栈做出最合适的选择。无论你是想在前端页面嵌入一个动态分析图表还是需要在报告里展示一份清晰的层级数据这篇文章都能给你一套立即可用的解决方案。1. 理解核心旭日图的数据结构与设计哲学在动手写代码之前我们得先搞清楚旭日图到底在表达什么。很多人把它简单理解成“多层饼图”这个类比有帮助但不够准确。饼图展示的是同一层级下各部分的占比而旭日图的核心是表达清晰的父子层级关系与逐级汇总。想象一下公司的组织架构整个公司是根节点下面有市场部、研发部等一级部门研发部下面又分前端组、后端组、测试组。一个理想的旭日图应该能让你一眼看出市场部和研发部各自占公司总资源的比例最内环。在研发部这个扇区内前端、后端、测试组又分别占研发部资源的比例第二环。你可以点击“研发部”扇区聚焦查看其下属组的详情这就是“数据钻取”。这种“逐级下钻、汇总回溯”的能力是旭日图区别于其他图表的核心。因此它的数据结构必须是严格的树形结构Tree Structure。每个节点都需要知道自己的父节点是谁根节点除外以及自己子节点的列表。在Echarts中这通常用一个嵌套的children数组来表示。一个典型的数据结构示例如下const sunburstData { name: 公司总资源, value: 100, // 根节点可以有一个总值也可由子节点汇总 children: [ { name: 市场部, value: 40, children: [ { name: 品牌组, value: 15 }, { name: 渠道组, value: 25 } ] }, { name: 研发部, value: 60, itemStyle: { color: #5470c6 }, // 可以为节点单独定义样式 children: [ { name: 前端组, value: 25 }, { name: 后端组, value: 30, children: [ // 可以继续嵌套更深层级 { name: Java服务, value: 18 }, { name: Go服务, value: 12 } ] }, { name: 测试组, value: 5 } ] } ] };注意value字段至关重要它决定了每个扇形块的角度大小。叶子节点没有children的节点的value是实际值。非叶子节点的value可以是其下所有叶子节点value的总和如果你手动提供了的话但更常见的做法是不提供非叶子节点的valueEcharts会自动计算其子节点值之和作为它的视觉大小。手动为中间节点设置value通常用于表示“该节点自身也有一个不归属于任何子类的值”这需要根据业务逻辑谨慎使用。理解了数据结构我们再来看看设计上的考量。旭日图通过半径来区分层级越靠近圆心层级越高。颜色通常用于区分同一层级下的不同兄弟节点或者通过颜色映射visualMap来反映另一个维度的数值如增长率、利润等。良好的设计应该避免颜色过多导致视觉混乱并确保相邻扇区有足够的对比度。2. 从零到一构建你的第一个Echarts旭日图理论说再多不如动手试一次。我们从一个最简单的例子开始逐步添加功能。首先确保你的项目已经引入了Echarts。可以通过CDN、npm安装或者直接下载echarts.min.js。2.1 基础绘制与核心配置我们先在HTML中准备一个容器然后用JavaScript初始化图表并设置最基本的数据。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title基础旭日图示例/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script style #chart-container { width: 800px; height: 600px; margin: 20px auto; } /style /head body div idchart-container/div script // 初始化图表实例 const chartDom document.getElementById(chart-container); const myChart echarts.init(chartDom); // 定义数据 - 一个简单的三层结构 const data { name: 产品线, children: [ { name: 消费电子, children: [ { name: 智能手机, value: 120 }, { name: 笔记本电脑, value: 80 }, { name: 智能穿戴, value: 45 } ] }, { name: 家电, children: [ { name: 空调, value: 65 }, { name: 冰箱, value: 90 }, { name: 洗衣机, value: 70 } ] }, { name: 工业设备, children: [ { name: 机床, value: 30 }, { name: 机器人, value: 55 } ] } ] }; // 配置项 const option { title: { text: 公司产品销售额分布旭日图, left: center, textStyle: { fontSize: 18 } }, tooltip: { trigger: item, formatter: {b}: {c} (占比{d}%) // b: 名称, c: 数值, d: 百分比 }, series: [{ type: sunburst, data: [data], // 注意series.data 接受一个数组通常只放一个根节点对象 radius: [15%, 80%], // 内半径和外半径可以是百分比或像素值 label: { rotate: radial, // 标签旋转方式radial沿径向tangential沿切向 fontSize: 14 }, emphasis: { // 高亮样式鼠标悬浮或点击时 focus: ancestor, // 聚焦模式self只高亮自己ancestor高亮自己和祖先descendant高亮自己和后代 itemStyle: { shadowBlur: 10, shadowColor: rgba(0, 0, 0, 0.5) } }, levels: [ // 可以针对每一层设置不同的样式 {}, { r0: 15%, r: 35%, itemStyle: { borderWidth: 2 }, label: { rotate: tangential } }, { r0: 35%, r: 70%, label: { align: right } }, { r0: 70%, r: 80%, label: { position: outside, padding: 3, silent: false } // silent: false 允许标签溢出 } ] }] }; // 使用配置项和数据显示图表 myChart.setOption(option); // 响应窗口大小变化 window.addEventListener(resize, function() { myChart.resize(); }); /script /body /html把这段代码保存为HTML文件用浏览器打开你就能看到一个基础但功能完整的旭日图了。鼠标悬停在扇区上会显示提示框点击扇区会触发高亮。这里有几个关键点series.data虽然我们的数据是一个树形对象但需要把它放在一个数组里[data]传入。Echarts支持多个旭日图系列并存但绝大多数情况下一个就够用了。radius定义了旭日图的内外半径。使用百分比如[15%, 80%]是响应式的好习惯它会相对于容器大小进行计算。levels这是一个非常强大的配置项允许你为每一层圆环定义不同的样式r0起始半径r结束半径、标签显示策略等。上面的配置示例展示了从内到外四层包括根节点层的不同样式。2.2 处理真实世界的数据格式化与转换实际项目中的数据很少是刚好符合Echarts要求的树形JSON。它们可能来自扁平化的CSV、关系型数据库表或者后端API返回的一个带有parentId字段的列表。这时就需要进行数据转换。假设你从后端拿到的是如下结构的扁平数据const flatData [ { id: 1, name: 公司, parentId: null, value: 100 }, { id: 2, name: 消费电子, parentId: 1, value: null }, { id: 3, name: 智能手机, parentId: 2, value: 120 }, { id: 4, name: 笔记本电脑, parentId: 2, value: 80 }, { id: 5, name: 家电, parentId: 1, value: null }, { id: 6, name: 空调, parentId: 5, value: 65 }, // ... 更多数据 ];我们需要一个函数将这个{id, parentId}结构的列表转换成嵌套的{name, value, children}结构。下面是一个通用的转换函数function flatListToTree(flatList, idKey id, parentKey parentId) { const map {}; const roots []; // 第一遍建立id到节点的映射并初始化children数组 flatList.forEach(item { map[item[idKey]] { ...item, children: [] }; }); // 第二遍构建树形结构 flatList.forEach(item { const node map[item[idKey]]; const parentId item[parentKey]; if (parentId null || parentId undefined || !map[parentId]) { // 没有父节点或父节点不存在则作为根节点 roots.push(node); } else { // 找到父节点将当前节点加入其children map[parentId].children.push(node); } }); // 通常旭日图只有一个根我们返回第一个根节点 return roots.length 0 ? roots[0] : { name: Root, children: [] }; } // 使用转换函数 const treeData flatListToTree(flatData); console.log(treeData); // 现在已经是嵌套格式了 // 然后将 treeData 作为 series.data 的值这个函数是处理此类数据转换的利器建议收藏。如果你的数据量非常大比如上万节点转换性能可能成为瓶颈这时可以考虑在服务端完成转换或者使用更高效的算法。3. 进阶技巧让旭日图真正“活”起来基础图表有了但要让它在业务场景中发挥价值还需要添加交互、动态更新和深度定制。3.1 实现数据钻取与状态管理数据钻取是旭日图最自然的交互。Echarts默认的点击行为是触发emphasis高亮。要实现点击后下钻到子层级我们需要监听click事件并动态更新图表数据。// 接续之前的代码在 myChart.setOption(option); 之后添加 let currentRootPath []; // 用于记录当前钻取路径例如 [公司, 研发部] myChart.on(click, function (params) { // params.data 是当前点击的数据节点 if (params.data.children params.data.children.length 0) { // 如果有子节点则下钻 currentRootPath.push(params.data.name); // 将当前点击的节点作为新的根节点重新渲染图表 const drillOption { series: [{ type: sunburst, data: [params.data], // 关键将点击的节点作为新的根 radius: [15%, 80%], label: { fontSize: 12 }, levels: [ /* 可以调整各级样式 */ ] }] }; // 更新标题显示当前路径 drillOption.title { text: 当前路径: ${currentRootPath.join( / )}, subtext: 点击中心区域或使用浏览器后退按钮返回, left: center }; myChart.setOption(drillOption, { notMerge: false }); // notMerge: false 表示合并选项只更新指定的部分 // 添加一个返回按钮或中心区域的点击返回逻辑示例点击中心空白处返回上一级 // 这里简单实现为在图表中心绘制一个返回提示实际项目可能用自定义组件更好 myChart.setOption({ graphic: [{ type: text, left: center, top: center, style: { text: 返回, fontSize: 14, fill: #999, cursor: pointer }, onclick: function () { goBack(); } }] }); } }); function goBack() { if (currentRootPath.length 0) { currentRootPath.pop(); // 这里需要你有办法获取到完整的数据树和当前路径然后重新计算根节点数据 // 一种简单方式始终保存完整的数据树副本然后根据路径查找节点 // 示例假设 fullData 是完整的树数据 let node fullData; for (const name of currentRootPath) { node node.children.find(child child.name name); if (!node) break; } const backOption { title: { text: currentRootPath.length ? 当前路径: ${currentRootPath.join( / )} : 公司产品销售额分布, subtext: }, series: [{ data: [node || fullData] // 如果路径找不到则回退到完整数据 }], graphic: currentRootPath.length 0 ? [{ /* 保留返回按钮 */ }] : null // 如果回到最顶层移除返回按钮 }; myChart.setOption(backOption); } }提示更优雅的做法是使用Echarts的dispatchAction来模拟高亮和动画或者结合前端路由如Vue Router、React Router来管理钻取状态使浏览器前进/后退按钮也能生效。对于复杂交互可以考虑将图表状态当前根节点路径纳入你的应用状态管理如Vuex、Redux。3.2 视觉优化与高级样式默认的旭日图可能看起来有些平淡。我们可以通过颜色、标签和动画来提升视觉效果。颜色策略同层级区分让同一父节点下的兄弟节点使用同一色系的不同明度/饱和度。跨层级连贯让子节点继承父节点的色系通过透明度或深浅变化来体现层级。使用视觉映射visualMap用颜色表示另一个数值维度如利润率。这需要你的数据节点有另一个数值字段如profitRate。// 在series配置中添加 visualMap const optionWithVisualMap { visualMap: { type: continuous, // 连续型映射 min: 0, max: 100, calculable: true, orient: horizontal, left: center, bottom: 20, inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] // 蓝绿色系渐变 }, // 映射到旭日图节点的哪个字段假设节点数据中有 profit 字段 dimension: profit, seriesIndex: 0 }, series: [{ type: sunburst, data: data, radius: [10%, 75%], // 节点样式颜色由 visualMap 根据 profit 字段决定 itemStyle: { borderColor: #fff, borderWidth: 2 }, label: { color: #333, fontWeight: bold, formatter: function (params) { // 自定义标签格式 // 只在外层显示标签避免重叠 if (params.treePathInfo.length 3) { // 例如深度大于等于3时 return {a|${params.name}}\n{c|${params.value}}; } return ; }, rich: { // 富文本样式 a: { fontSize: 12, lineHeight: 16 }, c: { fontSize: 10, color: #999 } } }, emphasis: { label: { show: true, fontSize: 16, fontWeight: bold } }, // 通过 levels 为不同层级设置渐变色 levels: [ { // 第一层根 itemStyle: { color: #ddd } }, { // 第二层 r0: 10%, r: 30%, itemStyle: { color: #c23531 }, label: { rotate: tangential } }, { // 第三层 r0: 30%, r: 55%, itemStyle: { color: #2f4554 } }, { // 第四层及更多 r0: 55%, r: 75%, itemStyle: { color: #61a0a8 } } ] }] };动画与过渡 Echarts为旭日图提供了平滑的初始动画和状态过渡动画。你可以通过animationDuration、animationEasing等参数控制。对于数据更新如下钻设置合适的animationDurationUpdate能让过渡更自然。series: [{ type: sunburst, // ... 其他配置 animationDuration: 1000, animationDurationUpdate: 1000, animationEasing: cubicOut }]3.3 性能优化与大数据处理当你的层级非常深比如超过10层或者叶子节点数量极多比如上千个时可能会遇到渲染性能问题表现为初始化卡顿、交互滞后。以下是一些优化思路数据聚合在数据源头进行聚合不要将最细粒度的数据全部塞给前端。例如对于销售额可以按产品大类、子类聚合而不是展示每一个SKU。虚拟渲染按需加载对于超深层级可以实现点击父节点时再异步加载其子节点数据而不是一次性加载整棵树。简化视觉元素设置label的show为false或者通过formatter和rich只在合适的层级显示标签。减少不必要的渐变、阴影效果itemStyle。适当减小radius让图表更紧凑。使用large模式Echarts 5 为旭日图提供了large属性当节点数量超过一定阈值默认largeThreshold为800时会启用优化渲染模式。series: [{ type: sunburst, large: true, largeThreshold: 1000, // 节点数超过1000时启用优化 // ... 其他配置 }]4. 避坑指南那些我踩过的“雷”在实际开发中有些问题文档里不会重点提但一旦遇到就很头疼。这里分享几个常见的“坑”和解决方案。坑一数据格式错误导致图表不显示或显示异常现象图表区域空白或只显示一个圆圈。排查打开浏览器开发者工具的Console查看是否有Echarts报错。检查数据格式根节点是否在数组中children字段名是否正确value字段在叶子节点是否存在且为数字使用console.log(JSON.stringify(yourData, null, 2))打印出完整数据结构仔细核对。解决确保数据结构是严格的树形每个节点至少包含name字段叶子节点包含value字段。坑二标签重叠或显示不全现象文字挤在一起或者外层扇区标签被截断。解决使用label.rotate策略radial或tangential。通过label.formatter函数根据扇区大小决定是否显示标签小扇区可以不显示或显示缩写。调整label.fontSize层级越深字体越小。利用levels为不同层设置不同的label配置例如外层标签position: outside。启用label.ellipsis在Echarts较新版本中来自动截断过长的文本。坑三颜色杂乱无章没有区分度现象相邻扇区颜色太接近或者颜色随机分配看不出规律。解决放弃默认的调色板手动定义颜色。可以为每一层定义一个颜色数组或者根据父节点颜色计算子节点颜色。使用colorBy: data默认让每个数据项独立决定颜色或colorBy: level让同一层级的扇区使用同一颜色通过levels[i].itemStyle.color设置。对于需要表达额外数值维度的情况坚决使用visualMap而不是用颜色同时表达类别和数值。坑四交互体验不佳现象点击下钻后不知道如何返回或者钻取后页面状态混乱。解决如上文所述实现一个清晰的“返回”机制可以是图表中心的按钮也可以是面包屑导航。在高亮emphasis时通过focus: ancestor或descendant让关联节点一起高亮增强视觉引导。利用tooltip.formatter提供更丰富的信息比如显示从根节点到当前节点的完整路径。5. 方案对比Echarts vs. Pyecharts如何选择文章开头提到了Pyecharts它是Echarts的Python接口。如果你的技术栈主要是Python比如用Django、Flask做Web后端或者用Jupyter做数据分析Pyecharts是一个极佳的选择它让你能在Python环境中生成Echarts图表然后以HTML形式嵌入页面或保存为图片。核心区别与选择建议特性Echarts (JavaScript)Pyecharts (Python)运行环境浏览器中依赖前端JavaScript引擎。Python环境中最终输出HTML/JS代码或图片。交互性原生、实时、丰富。所有交互在用户浏览器中即时响应。通过渲染的HTML同样支持Echarts的所有交互但生成过程在服务端。动态数据非常适合与前端框架Vue/React结合通过Ajax/WSS实时更新数据图表动态变化。更适合一次性生成静态图表。虽然可以通过Jupyter的notebook模式获得一些交互或在Web框架中通过刷新页面更新但实时性不如纯前端。开发流程前端开发者更熟悉调试方便浏览器DevTools。数据分析师、Python后端开发者更熟悉可与Pandas、NumPy等库无缝衔接进行数据预处理。输出形式网页中直接渲染的Canvas/SVG。主要是HTML文件也可通过selenium或pyppeteer等工具渲染保存为PNG/PDF等静态图片。适用场景需要强交互、实时数据更新的Web应用如商业智能(BI)仪表盘、实时监控大屏、配置复杂的后台管理系统。数据分析报告、自动化报表生成、学术论文插图、一次性数据探索。在Jupyter中快速可视化分析结果尤其方便。Pyecharts旭日图代码片段示例from pyecharts import options as opts from pyecharts.charts import Sunburst data [ opts.SunburstItem( nameA, children[ opts.SunburstItem(nameA1, value10), opts.SunburstItem(nameA2, value20), ], ), opts.SunburstItem( nameB, children[ opts.SunburstItem(nameB1, value15), opts.SunburstItem(nameB2, value25, children[ opts.SunburstItem(nameB2a, value10), opts.SunburstItem(nameB2b, value15), ]), ], ), ] c ( Sunburst() .add( series_name, data_pairdata, highlight_policyancestor, radius[0, 90%], sort_null, levels[ opts.SunburstLevelOpts( r00%, r25%, itemstyle_optsopts.ItemStyleOpts(border_width2), ), opts.SunburstLevelOpts( r025%, r60% ), opts.SunburstLevelOpts( r060%, r80%, label_optsopts.LabelOpts(positionoutside), ), ], ) .set_global_opts( title_optsopts.TitleOpts(titlePyecharts 旭日图示例), tooltip_optsopts.TooltipOpts(formatter{b}: {c}), ) .render(pyecharts_sunburst.html) )如何选择我的经验是看核心需求在哪里。如果你的项目重心是提供一个有深度交互、数据实时流动的在线分析工具那么直接使用Echarts前端库是更直接、体验更好的选择。如果你的主要工作是用Python进行数据分析需要将分析结果以图表形式快速呈现并嵌入报告或简单的Web页面那么Pyecharts能极大提升你的工作效率避免在JavaScript和数据格式转换上花费过多时间。两者底层都是Echarts学好一个另一个的原理也就通了。最后再分享一个我自己的习惯在复杂图表开发初期我会先用Pyecharts在Jupyter里快速把数据和基本样式跑通因为Python处理数据和试错更快。确定好视觉和交互效果后再把逻辑移植到前端的Echarts实现中这样可以节省大量在前端反复调整数据格式和样式的时间。