上个月帮一家做风资源评估的客户做数据可视化报表,对方直接甩过来几万条测风塔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或数据库表,核心字段就三个:时间、风速、风向。我给客户处理的那份数据大致长这样:
| time | ws | wd |
|---|---|---|
| 2024-01-01 00:00 | 5.2 | 350 |
| 2024-01-01 00:10 | 4.8 | 355 |
| 2024-01-01 00:20 | 6.1 | 3 |
| ... | ... | ... |
这里ws单位是m/s,wd是风向角度,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度。方位中心角度分别是N=0,NNE=22.5,NE=45,ENE=67.5,E=90,依此类推。判断一条风向记录属于哪个方位,最简单的方式是拿角度除以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,也就是角度轴,类型设为category,data传16个方位名称,startAngle设为90,这一步是为了让第一个分类N出现在正上方,符合看风玫瑰图时“上北下南”的直觉。第四部分是series,每个风速等级一个series,类型都是bar,coordinateSystem必须显式写成polar,stack名称统一,这样Echarts才知道要在极坐标下做堆叠。
这里最容易踩的坑是忘了写coordinateSystem。不写这个字段,series会默认走直角坐标的grid,结果就是图渲染成普通柱状图,完全不会绕圈排列。如果你发现配置了polar但柱子没有出现在极坐标里,先检查这个字段。
3.2 完整可运行代码
先给一个不依赖框架、开箱即跑的完整版本,用script标签引入Echarts就能看效果。数据我用的是模拟数据,16个方位各5个风速等级,每个方位合计100,代表该方位内的频率占比。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <title>风力风速玫瑰图</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <style> #chart { width: 800px; height: 600px; } </style> </head> <body> <div id="chart"></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就要改成所有方位频次总和或略高一点。很多人忽略这个,数据最大值是78,max还停在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 ref="chartRef" style="width: 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使用同一个stack |
| N方位不在正上方 | angleAxis未设置startAngle | 设置startAngle: 90 |
| 最长花瓣被截断 | radiusAxis.max小于数据最大值 | 动态计算最大值并向上取整 |
| tooltip显示undefined | formatter里使用了错误的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直接打印聚合结果确认无误后,再接入图表。配色上别贪多,一套低饱和度的蓝色系或青色系就够,颜色太多会让风速分档的层级感消失。做玫瑰图这件事,数据干净、口径清晰,图自然就立住了。