尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

动态旭日图实战:用Plotly实现多层级占比可视化

发布时间:2026/9/29 3:56:26

资讯中心
01
ARTICLE

动态旭日图实战:用Plotly实现多层级占比可视化

动态旭日图实战:用Plotly实现多层级占比可视化
1. 项目概述为什么需要动态旭日图最近做数据平台改造手里正好压着一个需求业务方有一套复杂的商品分类体系一级类目下面套二级、二级下面套三级最深能怼到六七层单看表格数据根本看不出整体占比关系。传统饼图、环形图只能表达一层维度嵌套饼图画出来像靶子似的扛不住多层级表达。老板提的要求很直接——上一张能在浏览器里跑的图点一下能展开收缩数据刷新后图表要跟着变别搞静态截图糊弄事。前后对比了ECharts和Plotly两套方案最终选了Plotly的Sunburst图表也就是旭日图。旭日图的核心价值在于用环形半径表达层级深度从圆心向外一层一层扩散数据量大时依旧能清楚看到“整体占比、局部占比、层级从属”三层信息。用Plotly做这件事还有个隐性优势——它本身是交互式渲染引擎悬停、点击、缩放、动态更新都是底层能力不需要额外手写事件绑定。在线环境直接引用CDN即可也支持服务端部署门槛比想象中低。这篇内容我按照自己的落地过程来写先说旭日图到底解决什么问题再讲数据结构和动态更新的关键点然后给出可直接复制的完整代码和换数据的方法最后把我实际踩过的坑列成速查表。适合三类读者一是要在网页里展示多层级占比的数据分析师二是正在给内部系统做可视化面板的前端开发三是想快速把Plotly用起来、不想啃英文文档的技术负责人。2. 旭日图背后的设计逻辑与数据层次解构2.1 旭日图为什么适合表达层次结构旭日图本质上是“饼图的径向扩展版”但不要把它理解成多个饼图套在一起。真正的场景是这样的假设你有一份全国门店销售数据需要同时看到大区—省份—城市—门店四层关系并且每一层的占比都要清晰呈现。普通饼图只能看大区占比树形图能看层级但面积占比不直观矩形树图能看占比但层级嵌套不够直观。旭日图把两者优势合在一起了。它读起来遵循两条基本规则第一同一个圆环层代表同一层级从圆心向外分别是第一层、第二层、第三层第二某一扇区的圆心角大小代表该节点在其父节点中的占比半径长度则不承担数值含义只代表层级深度。这就意味着用户第一眼关注颜色和扇形角度就能快速判断“哪个分支最大”随后再沿着半径一层层深入观察子级之间的相对关系。这是很接近人脑认知习惯的表达方式。人类判断大小最自然的视觉通道是面积和角度而旭日图恰好让面积扇区面积和数值在同一个维度上绑定。与之对比树形图虽然也表达层级但叶子节点的面积占比容易误导人因为树形图的面积常常不是按数值比例分配的旭日图则不会出现这个问题每一段弧长和半径共同决定的面积严格对应数据占比。2.2 层次数据的组织方式从扁平到层级Plotly的旭日图接收的数据结构并不要求是嵌套JSON它内部支持“扁平表”结构——就是俗称的宽表。你需要准备三列一列是每个节点的ID或者名称一列是父节点ID一列是数值。比如“华东—上海市—浦东新区—张江门店”这条链路会拆成四行数据华东父节点为空数值为所有子节点合计上海市父节点为华东数值为上海合计浦东新区父节点为上海市数值为浦东合计张江门店父节点为浦东新区数值为门店实际销售额你可能注意到数值在这里出现了“重复累计”父节点数值等于子节点数值之和。这是Plotly旭日图的一个关键机制——父节点数值并不需要在数据中提供Plotly会自动对子节点求和所以父节点那一行的数值可以直接留空或者填0不影响展示。实测下来显式给出父节点数值反而可能引发显示异常尤其是当子节点数值之和与父节点数值不一致时扇区会出现比例失衡。实操中我更推荐一个做法在数据库查询阶段就用SQL把层级字段拼接好用WITH RECURSIVE递归查询生成父子两列前端只负责接数据不做二次聚合。这样做的好处是数据流清晰、排查问题容易尤其当数据量达到数万行时前端递归处理会比数据库慢得多而且SQL底层的递归树生成逻辑更容易配合索引优化。2.3 颜色映射与视觉编码Plotly旭日图支持三种颜色模式新手最容易在这里犯迷糊。第一种是统一颜色所有扇区同一种颜色只靠弧长区分第二种是按父节点统一色系同一父节点下的子节点颜色相近但深浅不同第三种是按数值连续映射数值大的深色、数值小的浅色。我的建议是如果业务场景需要快速对比“各大区销售额”用按数值连续映射如果重点是“看层级结构是否健康”例如看库存结构、组织结构用按父节点分组色系。颜色模式在Plotly里对应branchvalues和marker.colors两组参数branchvalues有两个取值total和remainder。total模式下父节点数值必须包含子节点的总和remainder模式下父节点数值表示的是“排除子节点后的剩余部分”。绝大多数业务场景用total就对了如果你想让父节点扇区单独显示“自身直接贡献”而非汇总值才考虑remainder。3. 在线Plotly动态旭日图的完整实现3.1 环境准备在线加载与本地文件选型在线绘制旭日图第一件事是决定Plotly的加载方式。目前主流的在线方案有两类一类是直接引用官方的CDN脚本另一类是把plotly.js打包进项目通过npm和打包工具管理。如果只是做原型验证或内部临时代码直接用CDN最高效。官方目前提供两个版本plotly-2.32.0.min.js完整版体积较大约4.5MB压缩后和plotly-gl2d等按需模块打包的精简版。对于旭日图这种基础图表完整版其实更方便因为后续可能还要画散点图、桑基图完整版一次到位省去模块组合的麻烦。如果项目本身是Vue或React工程我建议走npm安装再引入plotly.js-dist-min这样可以利用打包工具做资源缓存和按需加载页面首屏速度会好看很多。需要注意的是不要用plotly.js主包里的tree-shaking期待自动消除无用模块——这个包的设计决定了它不支持细粒度tree-shaking想极致精简得手动引用plotly.js-basic-dist或自己去官方定制构建页面做模块裁剪。我的实操配置大概是这样的开发环境用本地npm包生产环境把plotly.js上传到自己的CDN服务器页面通过script标签异步加载加载完成后再初始化图表。这个方案比纯CDN稳定不受第三方网络波动影响比全部打进业务包更合理因为plotly的缓存周期可以和业务代码解耦。3.2 核心代码十分钟跑通的动态旭日图下面给出一段我直接在生产环境用过的代码用纯HTMLJavaScript实现数据源模拟从后端接口动态获取的场景。为了大家方便我先把数据写死展示完结构之后再讲动态替换。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title动态旭日图示例/title script srchttps://cdn.plot.ly/plotly-2.32.0.min.js charsetutf-8/script style #sunburst-container { width: 100%; height: 600px; margin: 20px auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .info-panel { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; font-size: 13px; color: #666; margin: 0 20px; } /style /head body div idsunburst-container/div div classinfo-panel idupdate-info等待数据加载.../div script // 模拟后端返回的树形数据扁平结构 function generateMockData() { const mockData { ids: [总部, 华东, 华北, 华南, 上海, 杭州, 北京, 广州, 芯片, 整机, 配件, 消费品, 工业品], parents: [, 总部, 总部, 总部, 华东, 华东, 华北, 华南, 上海, 上海, 杭州, 北京, 广州], values: [0, 4200, 3100, 2800, 2600, 1600, 3100, 2800, 1200, 900, 500, 1100, 1700] }; return mockData; } function createSunburst(data) { const trace { type: sunburst, ids: data.ids, labels: data.ids, // 显示名称 parents: data.parents, values: data.values, branchvalues: total, // 点击定位时的过渡动画时长毫秒 transition: { duration: 500, easing: cube-in-out }, marker: { colors: [ #636EFA, #EF553B, #00CC96, #AB63FA, #FFA15A, #19D3F3, #FF6692, #B6E880 ], line: { color: #ffffff, width: 2 } } }; const layout { margin: { t: 30, l: 10, r: 10, b: 10 }, paper_bgcolor: rgba(0,0,0,0), plot_bgcolor: rgba(0,0,0,0), sunburstcolorway: [#636EFA, #EF553B, #00CC96, #AB63FA, #FFA15A], extendsunburstcolorway: true, font: { family: Microsoft YaHei, sans-serif, size: 14 } }; const config { responsive: true, displaylogo: false, modeBarButtonsToRemove: [lasso2d, select2d] }; Plotly.react(sunburst-container, [trace], layout, config); } // 模拟接口请求 function fetchDataAndRender() { document.getElementById(update-info).textContent 正在拉取最新数据...; setTimeout(() { const freshData generateMockData(); // 模拟数值变化展示动态更新效果 freshData.values[5] Math.round(1600 * (0.8 Math.random() * 0.4)); freshData.values[8] Math.round(1200 * (0.8 Math.random() * 0.4)); createSunburst(freshData); const now new Date(); document.getElementById(update-info).textContent 最后更新${now.toLocaleTimeString()}数据版本 v${Date.now()}; }, 500); } // 首次渲染 fetchDataAndRender(); // 开启自动刷新每5秒一次模拟动态数据流 setInterval(fetchDataAndRender, 5000); /script /body /html这段代码的核心逻辑不复杂但有几个细节我要单独强调第一我用Plotly.react()而不是Plotly.newPlot()。这是Plotly官方推荐的做法react会对比新旧数据之间的差异只更新变化的trace而newPlot每次都会把整个DOM替换掉。在动态场景下react的渲染效率和过渡动画平滑度明显更好。实测一个包含300个节点的旭日图react方式刷新数据时浏览器主线程阻塞时间大概只有newPlot方式的20%。第二transition参数控制了点击节点时的缩放过渡动画。旭日图在点击某一扇区后会以该扇区为新的“根”重新布局这是最有交互感的操作之一。如果没有设置transition跳转是瞬时的视觉上会显得突兀设置duration: 500和easing: cube-in-out后缩放过程会比较柔和。第三extendsunburstcolorway这个参数值得单独说。旭日图的子节点层级深当节点数量超过sunburstcolorway配色的数量时Plotly会自动循环使用颜色但这会导致相邻两个层级的颜色容易混淆。开启extendsunburstcolorway后Plotly会在基础色系上做亮度偏移生成新颜色视觉上既保留了色系一致性又避免了完全相同的颜色出现在不同层级。3.3 动态更新的机制接口对接与数据流设计把模拟数据换成真实后端接口本质上只需要替换fetchDataAndRender函数的内部实现。我建议用标准fetch方法配合AbortController来处理请求竞态问题——这一点非常容易被忽略。动态数据有个经典问题前端每5秒拉一次接口如果某一次请求因为网络原因耗时超过5秒后一次请求可能先返回前一次请求后返回最终把旧数据覆盖到新数据上图表就会闪回。解决方式很简单let abortController null; async function fetchDataAndRender() { // 取消上一次未完成的请求 if (abortController) { abortController.abort(); } abortController new AbortController(); try { const response await fetch(/api/sunburst-data, { signal: abortController.signal }); const data await response.json(); createSunburst(data); } catch (err) { if (err.name AbortError) { console.log(请求已被新的请求替代忽略旧响应); return; } console.error(数据加载失败, err); } }这里AbortController的作用是新请求发起前主动取消旧请求浏览器层面直接断开旧的HTTP连接后端即使返回了数据也不会触发前端的回调逻辑。在弱网环境下这个细节能有效避免“图表来回跳”的诡异现象。动态更新频率也需要根据业务合理设置。我见过有人把刷新间隔设成1秒图表面板确实很“动态”但用户的视觉注意力完全被动画带走了根本没法静下心分析数据。更合理的方案是双层更新首次加载后立即渲染之后每30秒静默刷新一次同时在下一次刷新前对数据做差异检测只有数据变化超过阈值才触发重绘。差异检测可以在前端实现也可以放在后端做版本号判断。后端每次返回时带一个data_version字段前端记录最近一次渲染的版本号新数据版本相同就跳过渲染。这样能省掉大量无意义的DOM更新对CPU占用率非常友好尤其是当页面还有其他图表同时滚动刷新时。4. 从实际业务出发案例与数据规模经验4.1 案例一销售区域结构的动态监控我落地这个方案的具体场景是销售看板。左侧是地图右侧就是这张旭日图展示“大区—省份—城市—渠道”四个层级每天的实时销售额。后端每10分钟同步一次数仓数据接口返回约2000行扁平结构数据。旭日图在这个场景的价值在于业务负责人能一眼看出华东大区今天占比多少华东下属哪个省份掉得最快进而决定要不要临时开会调整策略。实现上额外加了两个小功能一是把数值格式化成带单位的字符串比如12.6万悬停提示里显示详细原始值二是给低于目标线的扇区加了淡红色高亮对比更直观。这里有一个值得注意的经验旭日图的悬停提示模板强烈建议定制。Plotly默认的悬停提示会显示id、parent、value这些原始字段不够友好。用hovertext或者customdata方式把百分比、环比涨跌幅、责任人等信息塞进去整个图表的决策辅助价值会高一个档次。const trace { type: sunburst, ids: data.ids, labels: data.labels, parents: data.parents, values: data.values, customdata: data.percentages, // 预先计算好的占比列表 hovertemplate: %{label}br销售额%{value:,.0f}br占比%{customdata}%extra/extra };extra/extra这段不要删它是用来隐藏悬停框右侧那个默认trace名称的。删掉或留空后悬停框会干净很多不会被“trace 0”这种无意义字样干扰。4.2 案例二组织结构与人才分布另一个同事用得比较多的是把旭日图当组织架构图用。公司管理层级一般是“集团—BG事业群—事业部—部门—小组”传统的组织架构图画出来密密麻麻高层的兄弟根本看不下去。换成旭日图后人员数量通过扇区面积表达一眼就能看出哪个事业部最臃肿哪个方向是公司资源投入的重心。这个场景给了一个启发旭日图并不只适合数值型数据它同样适合表达“某维度下的成员数量分布”。你完全可以把数值列换成人数、工单数、代码行数、调用次数等。Plotly对数值的正负没有特殊限制但如果数据包含负数扇形角度的计算会乱建议先做数据清洗把负数节点踢掉或归一化到0以上。4.3 大数据量下的性能与交互优化关于性能我最想分享的一组真实数据单个旭日图节点数在2000以内时Plotly的渲染时间基本都在100ms以内体感上完全无感节点数超过5000时首次渲染可能有明显卡顿滚动和缩放也会出现掉帧超过1万个节点时就不建议继续用单张旭日图硬扛了应该考虑数据聚合、抽样或改成多级联动图表。性能优化有几个实用手段按投入产出比排序后端聚合接口返回前把数据做一次预汇总减少节点数。举例来说最细粒度是门店级但展示端并不需要每个门店都可见可以设置一个小额阈值低于阈值的门店合并进“其他”节点。懒加载初始只渲染前两层用户点击进入第三层时再动态补充数据。这个策略我在项目里用过前端代码稍复杂但大促期间页面卡顿的问题就此没有再出现。关闭动画数据量大的场景把transition.duration设为0能显著减少重绘消耗。动画在数据量小时是锦上添花数据量大时就是白白浪费CPU。我特别想提醒一点不要在大屏项目里开自动刷新且不做版本判断。大屏通常用的是工控机或低配主机CPU和GPU资源有限。我在一个智慧园区项目里踩过这个坑五块屏幕、十二张图表、全部3秒轮询结果工控机CPU直接拉满后台程序全部卡死。后来把所有图表全部加上数据版本号判断数值没变化就不重绘CPU占用率从90%降到了15%以下。5. 常见问题与排错技巧实录5.1 节点不显示或比例异常这是旭日图最常见的问题百分之八十的原因是parents字段有错。我总结了一个快速自查顺序先检查根节点的父节点是否为空字符串而非null或undefined再检查是否所有父节点ID都真实存在于ids列表中最后检查是否存在浮空节点——即某个节点在ids中有名字但它指向的父节点并不存在。Plotly对数据校验相对宽松某些错误不会直接报错而是表现为“该节点消失”或“整条链路消失”排查起来很费劲。我的建议是把数据接进来后先写一段简单的校验在浏览器控制台跑一遍把所有浮空节点找出来function validateSunburstData(ids, parents) { const idSet new Set(ids); const floatingNodes []; parents.forEach((p, idx) { if (p ! !idSet.has(p)) { floatingNodes.push({ node: ids[idx], missingParent: p }); } }); if (floatingNodes.length 0) { console.error(发现浮空节点, floatingNodes); } else { console.log(数据校验通过); } }这个函数帮我省了好几次半夜加班建议大家封装到工具函数里每次接新数据源都跑一遍。5.2 数值更新后图表闪烁或跳动图表闪烁通常和两个因素有关一是本身用了newPlot导致的整体重建解决方式是换成react前面已经说过二是异步请求的竞态问题用AbortController解决前面也给了代码。还有一种隐蔽情况数据一直不变但扇区却在轻微抖动。这个大概率是内部浮点精度问题。Plotly计算角度时会基于数值占比如果数值带了很长的浮点数尾数比如12345.67890123可能在每次重绘时因为浮点运算微小的差异导致角度有极小的偏移。视觉上表现为边缘闪烁。解决方法是数据入库时就做四舍五入或者前端渲染前统一Math.round(val * 100) / 100处理。5.3 点击节点后无法返回上级Plotly旭日图默认支持点击扇区进入下一层但不提供“返回上一层”的按钮。很多用户点了子级之后会迷路面对一个局部展开了的扇形不知所措。有两种处理方式官方封装的方式是在布局中加一个返回按钮监听plotly_sunburstclick事件手动把图表重新设置到指定层级简单粗暴的方式是右侧画一个层级指示器显示当前根节点路径点击指示器某一级就可以返回。我的实现方案是监听点击事件并把当前根节点显示在图表标题里let currentRootId 总部; sunburstContainer.on(plotly_sunburstclick, (event) { const points event.points; if (points points.length 0) { const nextRoot points[0].id || 总部; currentRootId nextRoot; document.getElementById(current-level).textContent 当前层级 currentRootId; } });注意plotly_sunburstclick事件里拿到的行为逻辑是点击非根节点进入该节点为根的子图点击当前根节点则返回上一层。所以还需要配合导航按钮做手动返回。这里我不展开全部代码思路就是维护一个根ID栈每次点击入栈返回按钮出栈然后调用Plotly.react()重新渲染。5.4 在线白屏或加载不出来CDN方式加载Plotly偶尔会遇到白屏或图表不渲染常见原因有加载脚本失败、容器高度为0、图表初始化时DOM节点尚未就绪。容器高度为0是新手最容易踩的坑。Plotly渲染图表时如果容器的宽或高为0会静默失败页面不报错但就是什么都不出现。解决方式是给容器预设固定高度或者用resize事件动态计算高度function resizeChart() { const container document.getElementById(sunburst-container); if (container) { Plotly.Plots.resize(container); } } window.addEventListener(resize, resizeChart);脚本加载失败的问题可以通过onerror回调捕捉并给出用户友好的提示也可以用document.fonts.ready等方式确保字体加载完成后再渲染避免中文字体未加载导致文字乱码。5.5 旭日图常见问题速查表现象原因解决方案某个节点不显示父子关系断裂或父节点不存在用validateSunburstData函数检测浮空节点扇区面积和数值不成比例branchvalues设置错误确认total或remainder语义父子数值按要求填写动态更新时整个图闪烁使用了newPlot改用Plotly.react()数据刷新后图表回到初始层级更新后未保持根节点状态用状态变量记录当前根ID重绘时传入level参数悬停提示显示默认trace名称未隐藏extrahovertemplate中加extra/extra中文标签乱码字体设置缺失layout.font中设置Microsoft YaHei或加载自定义字体颜色层级混淆节点太多颜色循环配置extendsunburstcolorway: true6. 实操心得与后续扩展方向整个项目做下来我最大的感受是旭日图本身是“数据表达武器库”里被低估的一个。很多团队一提层次结构就想到树形组件或可折叠表格但那张跨层级占比的关系图用旭日图呈现后业务方的理解成本确实降了一大截。代码层面我最后推荐一个组合拳数据用后端递归SQL生成扁平表前端用Plotly.react渲染更新频率用版本号控制鼠标事件用官方事件API接管登录权限和操作日志挂在容器外面。这套组合帮我省了很多迭代时间目前已经沉淀成团队内部的可复用组件换个接口字段就能直接套用到新需求上。还有一个细节分享给做数据产品的人旭日图的“动态”不只是自动刷新。我后来在页面上加了时间轴滑块用户可以手动拖到任意历史时间点图表就会加载那个时间点的数据快照。这个交互方式比单纯自动轮询更有分析感同时加载逻辑和自动刷新共用一套渲染函数后者只多一个定时器而已。如果你的场景是历史数据回看这个扩展方向值得一试。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。