46 雷达图绘制:数据多边形与填充

📅 2026/7/11 22:22:11
46 雷达图绘制:数据多边形与填充
46 雷达图绘制数据多边形与填充前言图46 雷达图绘制数据多边形与填充 运行效果截图HarmonyOS NEXT在上一篇文章中我们绘制了雷达图的三层网格背景。本文将在此基础上绘制数据多边形——将 6 维数值映射到网格上的点连接成多边形并填充半透明颜色。这是雷达图的核心视觉元素用户通过它一眼就能看出自己在各个维度的强弱分布。本文以 [HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets) 为例详解数据多边形的绘制。鸿蒙官方·Canvas 样式 APIdeveloper.huawei.com项目源码[HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)图雷达图数据多边形——6维分值映射到半径连接成填充多边形6维分值数组0-100归一化分值 / 100 × radius计算数据点坐标同网格顶点角度beginPath依次连接6点closePath 闭合fill 半透明填充rgba带透明度stroke 描边实线外框一、数据结构1.1 RadarPoint 接口exportinterfaceRadarPoint{label:string// 维度名称如 能量value:number// 数值 0-100如 62}// 实际数据示例this.radarData[{label:能量,value:62},{label:思维,value:65},{label:行动,value:55},{label:情绪,value:72},{label:社交,value:60},{label:细节,value:70}]1.2 数值到坐标的映射每个维度的数值0-100通过以下公式映射到画布坐标// 数值 → 半径距离的比例constratiopoint.value/100// 0.0 ~ 1.0// 半径距离constrradius*ratio// 距中心点的像素距离// 顶点坐标constxcxr*Math.cos(angles[i])constycyr*Math.sin(angles[i])数值映射示意数值 0 → r 0 → 点位于中心 数值 50 → r r/2 → 点位于中层网格 数值 100 → r r → 点位于最外层网格顶点二、数据多边形绘制2.1 连接所有顶点// HandwritingRadar.ets — 数据多边形绘制ctx.beginPath()// 开始新路径this.data.forEach((p,i){// 当前维度对应的半径数值比例 × 最大半径 × 动画进度constrradius*(p.value/100)*this.drawProgressconstxcxr*Math.cos(angles[i])constycyr*Math.sin(angles[i])if(i0){ctx.moveTo(x,y)// 第一个顶点}else{ctx.lineTo(x,y)// 依次连接到后续顶点}})ctx.closePath()// 闭合回第一个顶点2.2 填充// 半透明填充品牌色暖棕28% 透明度ctx.fillStylergba(168,144,122,.28)// ✅ fill 必须在 stroke 之前ctx.fill()// 实线描边ctx.strokeStyleAppColors.PRIMARY// 主品牌色 #A8907Actx.lineWidth2ctx.stroke()为什么 fill 要在 stroke 之前在 Canvas 中fill()和stroke()的调用顺序不会影响视觉效果两者独立渲染但建议先 fill 再 stroke——即使 fill 半透明颜色也不会覆盖到 stroke 线上。2.3 完整代码// 数据多边形从中心向外扩散动画 ctx.beginPath()this.data.forEach((p,i){constrradius*(p.value/100)*this.drawProgressconstxcxr*Math.cos(angles[i])constycyr*Math.sin(angles[i])if(i0)ctx.moveTo(x,y)elsectx.lineTo(x,y)})ctx.closePath()ctx.fillStylergba(168,144,122,.28)ctx.fill()ctx.strokeStyleAppColors.PRIMARYctx.lineWidth2ctx.stroke()三、多层数据叠加DuetReport在双人合盘场景中需要在同一个雷达图上绘制两个数据多边形3.1 两套颜色系统// 我的数据实线ctx.strokeStyleAppColors.WARM// 暖色 #D9A689ctx.fillStylergba(217,166,137,.18)// 绘制我的多边形// ...同第一节代码// 对方的数据虚线ctx.setLineDash([5,4])// 虚线区分ctx.strokeStyleAppColors.ROSE// 玫瑰色 #C9A0A0ctx.fillStylergba(201,160,160,.18)// 绘制对方多边形// ...ctx.setLineDash([])// 恢复实线3.2 样式对比属性我的多边形对方多边形描边颜色WARM #D9A689ROSE #C9A0A0描边线型实线setLineDash([])虚线setLineDash([5,4])描边宽度2px2px填充颜色rgba(217,166,137,.18)rgba(201,160,160,.18)四、填充样式进阶4.1 渐变填充对于单色半透明填充项目使用了固定颜色。但也可以扩展为渐变填充/** * 从中心向外渐变 * 中心透明度低 → 边缘透明度高 */constgradientctx.createRadialGradient(cx,cy,0,cx,cy,radius)gradient.addColorStop(0,rgba(168,144,122,0.50))// 中心较实gradient.addColorStop(1,rgba(168,144,122,0.05))// 边缘透明ctx.fillStylegradient ctx.fill()4.2 按数据值动态颜色/** * 根据平均分动态调整填充颜色 */functiongetFillStyle(data:RadarPoint[]):string{constavgdata.reduce((sum,p)sump.value,0)/data.lengthif(avg70)returnrgba(168,144,122,.32)// 高分 → 较深if(avg50)returnrgba(168,144,122,.22)// 中等returnrgba(168,144,122,.14)// 低分 → 较浅}4.3 不同维度的色值区域不建议// ❌ 不推荐每个维度不同颜色 → 视觉效果杂乱// 通常建议整个数据多边形使用统一的半透明色五、完整的绘制时序5.1 绘制顺序Canvas 绘图的层级顺序非常重要——后面的绘制会覆盖前面的内容① clearRect(0, 0, W, W) 清除画布 ② 三层网格同心正六边形 → 底层 ③ 数据多边形填充 → 中层半透明 ④ 数据多边形描边 → 上层实线 ⑤ 顶点标签文字 → 最上层5.2 叠加效果示意最顶层 ⑤ 标签文字 ④ 数据多边形描边线宽 2 ③ 数据多边形填充半透明 28% ② 三层网格描边线宽 1 最底层 ① 画布清除透明背景六、动画与数据多边形的结合6.1 从中心向外扩散// 通过 this.drawProgress0→1控制数据多边形的半径比例constrradius*(p.value/100)*this.drawProgress// drawProgress0 时 → 所有顶点都汇聚到中心// drawProgress0.5 → 数据多边形绘制到 50%// drawProgress1 → 数据多边形到达最终位置6.2 网格与数据多边形的动画独立// 网格直接绘制不依赖 drawProgressfor(letlayer1;layer3;layer){constrradius*(layer/3)// ... 直接绘制完整网格}// 数据多边形依赖 drawProgress从中心向外生长this.data.forEach((p,i){constrradius*(p.value/100)*this.drawProgress// ...})为什么网格不做动画网格是参照系需要一开始就完整显示数据多边形是数据展示通过扩散动画吸引视觉焦点七、样式调整参数7.1 项目中可调样式参数参数当前值效果调整建议填充透明度0.28淡淡显色调高→更深调低→更透描边宽度2px清晰可见调高→更突出描边颜色PRIMARY #A8907A品牌棕可按情绪类型变化7.2 不同情绪类型的数据多边形配色/** * 根据人格类型选择不同的数据多边形颜色 */functiongetRadarColor(personalityType:string):{fill:string,stroke:string}{switch(personalityType){case温和理性型:return{fill:rgba(168,144,122,.28),stroke:#A8907A}case开朗热情型:return{fill:rgba(217,166,137,.28),stroke:#D9A689}case内敛安静型:return{fill:rgba(91,127,182,.20),stroke:#5B7FB6}case敏锐果断型:return{fill:rgba(155,174,168,.28),stroke:#9BAEA8}case温柔体贴型:return{fill:rgba(201,160,160,.28),stroke:#C9A0A0}default:return{fill:rgba(168,144,122,.28),stroke:#A8907A}}}八、常见问题8.1 fill 后 stroke 看不见// 场景fill 颜色完全不透明盖住了 strokectx.fillStyle#A8907A// ❌ 完全不透明ctx.fill()ctx.stroke()// stroke 被 fill 盖住// 原因fill 覆盖了 stroke 的区域// ✅ 解决使用半透明 fill或先 stroke 再 fillctx.fillStylergba(168,144,122,.28)// 半透明ctx.fill()ctx.stroke()// 可见8.2 数据多边形超出网格// 场景某维度 value 100导致顶点超出最外层网格// 原因输入数据未校验constsafeValueMath.min(p.value,100)// 限制最大 100constrradius*(safeValue/100)8.3 数据点少于 6 个// 场景只传入 4 个数据点// 结果绘制出四边形而非六边形// 解决统一使用 6 个维度不足时补 0constpaddedData[...this.data]while(paddedData.length6){paddedData.push({label:,value:0})}十、最佳实践与性能优化10.1 代码组织规范以下规范可显著提升代码可读性与可维护性分层解耦UI 逻辑pages/与数据操作DAO/Service严格分离不在 build() 中调用数据库组件拆分单个Component保持 100~200 行以内大型组件拆分为Builder子函数令牌约束颜色、字号、间距统一引用AppColors/AppFonts/AppAnimations禁止魔数错误处理异步方法统一返回PromiseT | null异常时hilog.error记录并返回null10.2 性能优化要点以下是常见性能优化措施的对比分析优化维度优化前优化后效果列表渲染ForEach全量渲染LazyForEach按需渲染内存降低 40-60%图片加载不指定尺寸指定width/height减少布局重计算数据库查询每次重新查询合理缓存 精确 Predicates查询速度提升 5-10x动画实现逐帧手动绘制animateTo属性动画稳定 60fps状态更新全局刷新State精细化作用域减少无效渲染总结本文详细解析了雷达图数据多边形的绘制数值映射value/100 × radius将 0-100 映射到画布坐标路径构建forEach连接 6 个顶点 →closePath闭合填充与描边半透明 rgba 填充 主色实线描边双人叠加两套颜色 实线/虚线区分双方动画整合drawProgress控制数据多边形从中心扩散生长下一篇文章将介绍雷达图标签自适应布局——如何在不同尺寸下确保文字清晰且不重叠。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力参考资源Canvas fillStyle / strokeStyle[HandwritingRadar 数据多边形源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)rgba 颜色解析Canvas createLinearGradient[DuetReportPage 双色雷达](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/DuetReportPage.ets)[AppColors 颜色常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppColors.ets)HarmonyOS 开发文档七、数据多边形动画效果雷达图的数据多边形支持入场动画——通过drawProgress变量控制绘制进度// 带动画的数据多边形绘制privatedrawDataPolygon(ctx:CanvasRenderingContext2D,scores:number[],progress:number):void{constsides6constradiusthis.canvasSize/2*0.75ctx.save()ctx.translate(this.canvasSize/2,this.canvasSize/2)ctx.beginPath()for(leti0;isides;i){constangle(Math.PI/3)*i-Math.PI/2// progress 从 0 到 1控制多边形展开动画constr(scores[i]/100)*radius*progressconstxr*Math.cos(angle)constyr*Math.sin(angle)i0?ctx.moveTo(x,y):ctx.lineTo(x,y)}ctx.closePath()ctx.fillStylergba(139, 92, 246, 0.3)// 品牌紫色半透明ctx.fill()ctx.strokeStylergba(139, 92, 246, 0.9)ctx.lineWidth2ctx.stroke()ctx.restore()}7.1 绘制进度驱动的动画StateWatch(onProgressChange)drawProgress:number0onProgressChange():void{this.draw()// 进度变化触发重绘}aboutToAppear():void{animateTo({duration:800,curve:Curve.EaseOut},(){this.drawProgress1.0})}如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力