
上个月帮一家做风资源评估的客户做数据可视化报表对方直接甩过来几万条测风塔10分钟平均风速和风向记录需求就一句话把全年的风向风速分布画出来。我第一反应就是用Echarts半小时把风力风速玫瑰图搭了出来后面又花了一下午打磨配色、交互和大屏适配。今天把这条实现路径从头到尾捋一遍包括数据分箱、方位映射、配置项拆解和一堆实际踩过的坑全部给你盘清楚。如果你也是刚接触数据可视化的前端开发手上正好有气象或环境数据不知道怎么落地或者你是气象、新能源、环保行业里要自己做报告图表的技术人员需要一个能直接照着改的模板这篇很值得看完。玫瑰图看起来花哨但本质不复杂核心就是极坐标加柱状图的组合把这一层窗户纸捅破后你就可以在Echarts里任意发挥。1. 风玫瑰图到底在画什么1.1 从测风数据到环形图形中间发生了什么业内说的风玫瑰图正规名字叫风向频率玫瑰图。它把统计周期内各个方位风向出现的频率按比例画在极坐标上看起来像一朵多瓣的玫瑰。如果再按风速等级做颜色分层就能把“风从哪来、有多大”一次性表达清楚。这个图在风电场选址、建筑通风分析、城市规划、空气污染物扩散评估里都是高频出现的标配图几乎每一个环境咨询报告里都能看到它。数据链路其实是这样的测风塔或者气象站按固定间隔记录风速和风向形成一条逐时数据流拿到这些原始记录后先按方位分成16个区间N、NNE、NE……再按风速大小分档最后统计每个方位在每个风速档里出现的次数换算成频率喂给Echarts渲染。这一串流程里数据清洗和分箱的功夫其实比写配置项更花时间因为原始数据不会直接告诉你“N方向刮了几天几级风”这些东西必须自己算。为什么要用圆形极坐标而不是普通的直角坐标因为风向是环形角度数据0度和360度在物理上属于同一个方向。如果硬塞进线性的X轴那351度和9度会被拉到坐标轴两端视觉上完全割裂。极坐标天然地把角度首尾相接保持了数据在方位上的连续性这才是风玫瑰图用极坐标的根本原因。1.2 方案选型堆叠极坐标和南丁格尔玫瑰图怎么选Echarts里做玫瑰图常见路径有两条。第一条是用polar极坐标坐标系加堆叠柱状图每个方位一根柱子柱子内部按风速等级分段这是气象行业标准风玫瑰图的画法最大的特点是方向信息和风速信息能同时呈现。第二条是用饼图的roseType模式把风速等级当成分类占比当数值画出来是南丁格尔玫瑰图但它本质上是展示单一维度占比的不包含风向信息只能叫风速玫瑰图严格意义上不是风向风速玫瑰图。两条路线适用场景完全不同。我之前做过一个风电项目业主方既要看主导风向又要看不同风速区间的分布那就必须用极坐标堆叠柱状图。而如果只是想在汇报PPT里展示“这个站点大风占比多少”南丁格尔玫瑰图会更清爽直观。两者不冲突甚至可以放在一个报告里互补。下面我把两条路线的实现都拆开讲。维度堆叠极坐标柱状图南丁格尔玫瑰图包含风向信息包含支持16方位不包含只有分类维度包含风速等级通过堆叠分段展示通过不同扇区展示适合场景风资源评估、环境分析报告风速占比概览、汇报展示实现复杂度偏中高配置项较多低几个关键配置搞定2. 数据预处理把逐时观测变成可统计的分箱数据2.1 原始记录的格式与清洗要点测风数据最常见的格式就是CSV或数据库表核心字段就三个时间、风速、风向。我给客户处理的那份数据大致长这样timewswd2024-01-01 00:005.23502024-01-01 00:104.83552024-01-01 00:206.13.........这里ws单位是m/swd是风向角度0度代表正北顺时针递增90度正东180度正南270度正西。开始统计之前必须做一轮清洗否则脏数据会直接把图带偏。我一般会过滤这几类风速小于0或者大于60的异常值风向不在0到360范围内的记录还有传感器故障产生的全零数据。判断故障不能只看单条我会按天统计如果某天数据缺失率超过20%这天整段弃用避免用残缺数据算频率。静风也要单独拎出来处理。行业惯例是风速小于0.2m/s有的项目用0.5m/s算静风静风没有明确的风向意义直接扔进16方位里统计会稀释风向分布。通常做法是把静风单独记一个占比在图中心用字母C标注或者放在图例注释里说明。如果不处理静风你的N方位可能被静风拉高好几个百分点主导风向判断就直接错了。2.2 16方位分箱一条公式替代大量if-else方位划分的原理不复杂一圈360度分成16个方位每个方位宽度22.5度。方位中心角度分别是N0NNE22.5NE45ENE67.5E90依此类推。判断一条风向记录属于哪个方位最简单的方式是拿角度除以22.5再四舍五入然后对16取模。用代码写就一行const DIRS [N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW]; function getDirIndex(degree) { // 360度取模防止边界值溢出 return Math.round((degree % 360) / 22.5) % 16; }这个公式我第一次看到时也愣了下其实原理就是先算角度落在哪个22.5度的区间里再映射到方位数组的下标。比如350度除以22.5约等于15.56四舍五入到16再对16取模得0对应N这跟气象上的直觉一致350度确实已经非常接近正北。再比如337.5度除以22.5正好等于15对应NNW。一个小技巧是如果风向数据里有0度和360度两种写法取模操作能把它们统一到N方向避免同一个方向被拆成两个方位。2.3 风速分档与统计口径的取舍风速分档没有全国统一的硬性标准气象行业常按蒲福风级划分但做风资源评估时更常用自定义区间。我习惯按0.2~1.5、1.6~3.3、3.4~5.4、5.5~7.9、8.0~10.7、大于等于10.8这样去分大致对应软风到轻风再到清劲风、强风的分界。区间划分直接影响图形效果分档太少图里只有两三圈颜色信息量不足分档太多颜色复杂图例都放不下。控制在5到6档比较合适。统计口径是另一个必须提前确认的事很多项目翻车就翻在这里。口径A是每个方位内各风速等级占该方位总频次的百分比这样16个方位各自合计100%适合分析“某个方向来的风主要有多大”。口径B是各方位各等级占全年总观测次数的百分比所有方位加起来才是100%适合看“全年风频到底集中在哪个方向”。两种口径画出来的图形态差异非常大如果跟报告其他章节的口径不一致数据对不上整张图就是废的。我的建议是动手前先跟业务方确认清楚然后在代码注释里写明白防止后面自己都忘了。3. 核心实现用堆叠极坐标柱状图画标准风玫瑰3.1 配置项逐段解析polar、angleAxis、radiusAxis、series标准风玫瑰图的实现思路本质上是把一个堆叠柱状图从直角坐标系“卷”到极坐标系里。直角坐标系里X轴的每个分类对应一根柱子在极坐标里每个分类对应一个角度方向直角坐标系的Y轴数值刻度在极坐标里变成半径方向的刻度。这样每根柱子从圆心向外生长不同风速等级通过堆叠呈现就形成了花瓣层层嵌套的视觉效果。关键配置拆开看是四个部分。第一部分是polar定义坐标系的位置和大小我一般写center和radius让图形居中并留出图例空间。第二部分是radiusAxis也就是半径轴它对应数值大小需要设置min、max和刻度数量max必须大于所有方位频次总和的最大值否则最长的那根花瓣会被截断。第三部分是angleAxis也就是角度轴类型设为categorydata传16个方位名称startAngle设为90这一步是为了让第一个分类N出现在正上方符合看风玫瑰图时“上北下南”的直觉。第四部分是series每个风速等级一个series类型都是barcoordinateSystem必须显式写成polarstack名称统一这样Echarts才知道要在极坐标下做堆叠。这里最容易踩的坑是忘了写coordinateSystem。不写这个字段series会默认走直角坐标的grid结果就是图渲染成普通柱状图完全不会绕圈排列。如果你发现配置了polar但柱子没有出现在极坐标里先检查这个字段。3.2 完整可运行代码先给一个不依赖框架、开箱即跑的完整版本用script标签引入Echarts就能看效果。数据我用的是模拟数据16个方位各5个风速等级每个方位合计100代表该方位内的频率占比。!DOCTYPE html html langzh-CN head meta charsetutf-8 / title风力风速玫瑰图/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style #chart { width: 800px; height: 600px; } /style /head body div idchart/div script const chartDom document.getElementById(chart); const myChart echarts.init(chartDom); const dirs [N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW]; const levels [ { name: 0.2~1.5 m/s, data: [28, 22, 18, 16, 16, 18, 22, 28, 32, 30, 24, 20, 16, 18, 22, 28] }, { name: 1.6~3.3 m/s, data: [34, 32, 30, 28, 28, 30, 32, 34, 36, 34, 32, 30, 28, 28, 30, 32] }, { name: 3.4~5.4 m/s, data: [24, 26, 30, 32, 32, 30, 26, 22, 20, 22, 26, 28, 30, 30, 28, 24] }, { name: 5.5~7.9 m/s, data: [10, 14, 16, 18, 18, 16, 14, 12, 8, 10, 12, 16, 20, 18, 14, 12] }, { name: 8.0 m/s, data: [4, 6, 6, 6, 6, 6, 6, 4, 4, 4, 6, 6, 6, 6, 6, 4] } ]; const series levels.map(level ({ name: level.name, type: bar, stack: total, coordinateSystem: polar, barWidth: 14, data: level.data })); const option { tooltip: { trigger: item, formatter: function (params) { const dir dirs[params.dataIndex]; return dir br/ params.seriesName params.value %; } }, color: [#aad3df, #8dbac8, #6ea6ba, #4a8ba6, #2e6e8e], legend: { bottom: 0, data: levels.map(level level.name) }, polar: { center: [50%, 54%], radius: 62% }, radiusAxis: { min: 0, max: 100, splitNumber: 4, axisLabel: { formatter: {value}% } }, angleAxis: { type: category, data: dirs, startAngle: 90, boundaryGap: true, axisLine: { show: true, lineStyle: { color: #ccc } }, axisTick: { show: false }, axisLabel: { fontSize: 12 } }, series: series }; myChart.setOption(option); window.addEventListener(resize, function () { myChart.resize(); }); /script /body /html这段代码直接复制到本地渲染出来就是一朵完整的16瓣风玫瑰。执行后你会看到每个方向都有5段颜色从圆心向外依次是低风速到高风速整体形态能看出来这个模拟站点的S、SSW方向低风速占比偏高而ENE、E方向的中高风速段更突出这就是玫瑰图一眼传达的信息。3.3 风向排序、堆叠顺序与刻度边界等细节代码跑通之后有几个细节必须说一下都是实际出图时影响观感和正确性的关键点。第一是堆叠顺序。series数组里第一个元素的柱子在最内侧最后一个在最外侧。所以一定要把风速最小的档位放在第一个风速最大的放在最后一个才能形成“低风速在内圈、高风速在外圈”的经典结构。如果顺序反了高风速在中间低风速包在外面视觉上非常怪异不熟悉的人甚至会误读数据。第二是barWidth。这个值控制每根柱子的角度宽度本质上是极坐标下的径向柱体宽度单位是像素。设太窄16根柱子之间空隙很大图形显得稀疏设太宽相邻柱体重叠导致边缘糊成一团。我一般根据图的尺寸在12到16之间调800像素宽的容器用14比较均衡。如果你的容器特别大或者特别小这个值要跟着动。第三是radiusAxis的max。我模拟数据的设计是每个方位合计100所以max直接设100。如果你用的是全年总次数口径max就要改成所有方位频次总和或略高一点。很多人忽略这个数据最大值是78max还停在50结果最长花瓣被裁掉一段报告里看着像一个被削了顶的饼极其尴尬。一个保险做法是写代码时动态计算数据最大值再乘1.1往上取整避免硬编码。第四是顺时针与逆时针方向。Echarts的angleAxis默认从startAngle开始顺时针排列分类也就是说如果startAngle是90那分类顺序是N、NNE、NE、ENE这样顺时针过去这正好符合气象上从北顺时针记录风向的习惯。如果你发现方位顺序是反的比如N后面跟着NNW那是角度轴方向设置反了可以调整分类数组顺序来处理。4. 衍生玩法用南丁格尔玫瑰图展示风速等级占比4.1 roseType的两种模式到底差在哪有时候你不需要方向信息只要展示风速等级的占比南丁格尔玫瑰图是很好的选择。Echarts饼图原生支持roseType看着很高级原理其实一句话把普通饼图的半径从固定值改成随数值变化的值。roseType有两个取值radius和area效果差异很明显。roseType半径与数值的关系视觉特点使用建议radius半径正比于数值面积会被平方放大大扇区非常突出需要强调某个大类的场景area面积正比于数值半径与数值平方根成正比视觉更均衡常规占比展示、数据对比我推荐默认用area。原因很简单人眼对面积更敏感area模式下面积之比等于数值之比看到的不容易被误导。radius模式下数值2倍半径只扩大到2倍但面积扩大到4倍视觉冲击远超实际差异这在汇报时很容易被质疑。另外提醒一点南丁格尔玫瑰图本质是饼图不要硬塞16个风向进去。16个扇区挤在一起标签全部叠起来完全没有可读性。这个图型更适合展示5到8个分类的场景风速等级这种粒度刚刚好。4.2 一个能直接跑的风速玫瑰图示例先看完整配置数据同样是模拟的6个风速区间合计100%。const option { tooltip: { trigger: item, formatter: {b}br/占比{c}% ({d}%) }, legend: { orient: vertical, right: 10, top: center }, series: [{ name: 风速等级占比, type: pie, roseType: area, radius: [20%, 75%], center: [45%, 50%], itemStyle: { borderRadius: 8, borderColor: #fff, borderWidth: 2 }, label: { formatter: {b} {d}% }, data: [ { name: 静风 C, value: 8 }, { name: 0.2~1.5 m/s, value: 24 }, { name: 1.6~3.3 m/s, value: 31 }, { name: 3.4~5.4 m/s, value: 22 }, { name: 5.5~7.9 m/s, value: 11 }, { name: 8.0 m/s, value: 4 } ] }] };这里radius设成[20%, 75%]意思是扇区最小半径占整体尺寸的20%最大可以到75%。这不是必须的但设置一个非零的最小半径可以让中心区域空出来视觉上更像花也能把静风的C标注放在中心位置。如果不设置内圈半径扇区会从圆心开始画所有扇区挤在一起不够清爽。4.3 标签、图例、提示框的定制技巧南丁格尔玫瑰图做出来以后最烦的就是标签重叠。扇区面积小的时候文字会挤成一团。我的处理思路是优先让标签放在外部用label配置line连接线指向扇区实在放不下的用labelLayout手动指定标签位置。Echarts从5.0开始对饼图标签的布局做了优化labelLayout: { hideOverlap: true }可以直接把重叠的标签隐藏掉虽然牺牲了部分信息但图面干净很多。图例的位置也要想清楚。我要么放右侧垂直排列要么放底部水平排列视容器宽高比而定。上面的示例放右侧因为它顶部的绘画区域比较小放底下会让整个图被压扁。提示框里我特意加了{d}%表示占整体的百分比这样用户悬浮到扇区时可以看到两个数一个是绝对占比一个是扇区之间的相对比例信息更完整。5. 大屏与工程化场景下的实战优化5.1 Vue3 Echarts的正确接入姿势现在前端项目很少直接拿原生HTML做图表了Vue3加Echarts是数据可视化大屏的高频组合。我在组件里一般这样写核心是管理好图表的生命周期避免内存泄漏和重复初始化。script setup import * as echarts from echarts; import { onMounted, onBeforeUnmount, ref } from vue; const chartRef ref(null); let chart null; function renderChart() { // 防止组件重复渲染导致的重复初始化 const existing echarts.getInstanceByDom(chartRef.value); if (existing) { chart existing; } else { chart echarts.init(chartRef.value); } chart.setOption(option); } function handleResize() { if (chart) { chart.resize(); } } onMounted(() { renderChart(); window.addEventListener(resize, handleResize); }); onBeforeUnmount(() { window.removeEventListener(resize, handleResize); if (chart) { chart.dispose(); } }); /script template div refchartRef stylewidth: 100%; height: 400px/div /template有个小经验echarts.init之前先调用echarts.getInstanceByDom检查一下是否已有实例这在Vue的热更新或者路由复用场景里特别管用。不然你会发现页面切换几次后控制台报“There is a chart instance already initialized on the dom”图越叠越多性能越来越差。5.2 自适应、销毁与多图表性能优化大屏项目的窗口尺寸变化频繁resize事件如果照单全收Echarts会频繁重绘流畅度明显下降。我会给resize处理函数加一个防抖时间控制在200毫秒左右。另外大屏设计稿一般是1920乘1080实际运行环境分辨率可能不同我习惯用CSS的transform scale对整个大屏容器做缩放Echarts容器本身保持设计稿尺寸这样图表不会因为rem换算出现文字和图形比例失调的问题。如果同页面有大屏套小屏、多个图表并存的情况还要注意渲染性能。Echarts默认用canvas渲染图表数量少没感觉数量一多动画和tooltip会开始掉帧。这种情况下可以关掉入场动画animation: false实测能省不少性能或者统一用一个resize监听去更新所有图表实例而不是每个组件各自监听。6. 常见问题与排查技巧实录6.1 高频报错和异常现象排查做这种图我前前后后踩过不少坑下面这些是提问频率最高的问题列个速查表遇到直接对照。现象常见原因解决办法图表区域空白容器高度为0或父元素display:none给容器设置明确高度在DOM挂载后再init柱状图出现在直角坐标里没有环绕排列缺少coordinateSystem: polar每个series都显式配置极坐标堆叠失效各风速等级从圆心各自散开stack名称不一致所有风速等级series使用同一个stackN方位不在正上方angleAxis未设置startAngle设置startAngle: 90最长花瓣被截断radiusAxis.max小于数据最大值动态计算最大值并向上取整tooltip显示undefinedformatter里使用了错误的params字段检查params.dataIndex和seriesName页面切换后图表重叠、性能下降重复init未销毁init前检查实例离开组件时dispose其中“渲染空白”是最容易忽略的因为Echarts的容器如果高度是0它不会报错只是默默画不出来。我在老项目里遇到过很多次查到最后发现是父元素flex布局后子元素高度塌陷给容器加一个明确height或者min-height就解决了。6.2 数据分析口径层面的坑技术层面的问题都好解决数据口径的坑才真要命。第一个坑是静风占比没有单独处理直接混进16方位里统计导致图形中心区域莫名凸起风向主导性被稀释。第二个坑是风速单位不统一有的测风塔返回m/s有的项目历史数据用的是km/h或节换算错一位小数点图的形态就完全不同。第三个坑是统计周期不一致有人用全年数据有人只用某个季度对比两个报告时图的量级完全不同读者很容易误解。我的习惯是在代码里加一个全局配置对象把统计周期的起止时间、风速单位、静风阈值、分档区间全部写清楚每次出图前检查一遍。还有一个容易被忽视的细节如果原始数据的观测频率不均匀比如前半年每10分钟一条后半年每小时一条直接按记录条数统计频率会引入偏差。正确的做法是先按小时或天做重采样再统计或者至少确认数据采集频率在整个周期内是一致的否则你的玫瑰图其实是在对比不同密度的数据而不是真实的风频。之前还有人问过我能不能用echarts-gl做3D玫瑰图视觉效果确实很拉风但我个人是劝退的3D透视会扭曲扇区的面积对比读者一眼看到的是柱子高低而不是准确的占比关系。可视化第一个原则是忠实地传达数据其次才是好看这一点在气象这种严谨行业里尤其重要。最后分享一个小经验。做这类图表真正花时间的往往不是配置代码而是跟业务方对齐口径和反复调整视觉效果。我一般会先把数据统计脚本写好用console直接打印聚合结果确认无误后再接入图表。配色上别贪多一套低饱和度的蓝色系或青色系就够颜色太多会让风速分档的层级感消失。做玫瑰图这件事数据干净、口径清晰图自然就立住了。