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

图:46 雷达图绘制:数据多边形与填充 运行效果截图(HarmonyOS NEXT)
在上一篇文章中,我们绘制了雷达图的三层网格背景。本文将在此基础上,绘制数据多边形——将 6 维数值映射到网格上的点,连接成多边形,并填充半透明颜色。
这是雷达图的核心视觉元素,用户通过它一眼就能看出自己在各个维度的强弱分布。本文以 [HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets) 为例,详解数据多边形的绘制。
鸿蒙官方·Canvas 样式 API:developer.huawei.com
项目源码:[HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)

图:雷达图数据多边形——6维分值映射到半径,连接成填充多边形
一、数据结构
1.1 RadarPoint 接口
export interface RadarPoint {
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)通过以下公式映射到画布坐标:
// 数值 → 半径距离的比例
const ratio = point.value / 100 // 0.0 ~ 1.0
// 半径距离
const r = radius * ratio // 距中心点的像素距离
// 顶点坐标
const x = cx + r * Math.cos(angles[i])
const y = cy + r * 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) => {
// 当前维度对应的半径(数值比例 × 最大半径 × 动画进度)
const r = radius * (p.value / 100) * this.drawProgress
const x = cx + r * Math.cos(angles[i])
const y = cy + r * Math.sin(angles[i])
if (i === 0) {
ctx.moveTo(x, y) // 第一个顶点
} else {
ctx.lineTo(x, y) // 依次连接到后续顶点
}
})
ctx.closePath() // 闭合回第一个顶点
2.2 填充
// 半透明填充(品牌色暖棕,28% 透明度)
ctx.fillStyle = 'rgba(168,144,122,.28)' // ✅ fill 必须在 stroke 之前
ctx.fill()
// 实线描边
ctx.strokeStyle = AppColors.PRIMARY // 主品牌色 #A8907A
ctx.lineWidth = 2
ctx.stroke()
为什么 fill 要在 stroke 之前?
在 Canvas 中,fill() 和 stroke() 的调用顺序不会影响视觉效果(两者独立渲染),但建议先 fill 再 stroke——即使 fill 半透明颜色也不会覆盖到 stroke 线上。
2.3 完整代码
// === 数据多边形(从中心向外扩散动画) ===
ctx.beginPath()
this.data.forEach((p, i) => {
const r = radius * (p.value / 100) * this.drawProgress
const x = cx + r * Math.cos(angles[i])
const y = cy + r * Math.sin(angles[i])
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
})
ctx.closePath()
ctx.fillStyle = 'rgba(168,144,122,.28)'
ctx.fill()
ctx.strokeStyle = AppColors.PRIMARY
ctx.lineWidth = 2
ctx.stroke()
三、多层数据叠加(DuetReport)
在双人合盘场景中,需要在同一个雷达图上绘制两个数据多边形:
3.1 两套颜色系统
// 我的数据(实线)
ctx.strokeStyle = AppColors.WARM // 暖色 #D9A689
ctx.fillStyle = 'rgba(217,166,137,.18)'
// 绘制我的多边形
// ...(同第一节代码)
// 对方的数据(虚线)
ctx.setLineDash([5, 4]) // 虚线区分
ctx.strokeStyle = AppColors.ROSE // 玫瑰色 #C9A0A0
ctx.fillStyle = 'rgba(201,160,160,.18)'
// 绘制对方多边形
// ...
ctx.setLineDash([]) // 恢复实线
3.2 样式对比
| 属性 | 我的多边形 | 对方多边形 |
|---|---|---|
| 描边颜色 | WARM #D9A689 | ROSE #C9A0A0 |
| 描边线型 | 实线(setLineDash([])) |
虚线(setLineDash([5,4])) |
| 描边宽度 | 2px | 2px |
| 填充颜色 | rgba(217,166,137,.18) | rgba(201,160,160,.18) |
四、填充样式进阶
4.1 渐变填充
对于单色半透明填充,项目使用了固定颜色。但也可以扩展为渐变填充:
/**
* 从中心向外渐变
* 中心透明度低 → 边缘透明度高
*/
const gradient = ctx.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.fillStyle = gradient
ctx.fill()
4.2 按数据值动态颜色
/**
* 根据平均分动态调整填充颜色
*/
function getFillStyle(data: RadarPoint[]): string {
const avg = data.reduce((sum, p) => sum + p.value, 0) / data.length
if (avg >= 70) return 'rgba(168,144,122,.32)' // 高分 → 较深
if (avg >= 50) return 'rgba(168,144,122,.22)' // 中等
return 'rgba(168,144,122,.14)' // 低分 → 较浅
}
4.3 不同维度的色值区域(不建议)
// ❌ 不推荐:每个维度不同颜色 → 视觉效果杂乱
// 通常建议整个数据多边形使用统一的半透明色
五、完整的绘制时序
5.1 绘制顺序
Canvas 绘图的层级顺序非常重要——后面的绘制会覆盖前面的内容:
① clearRect(0, 0, W, W) 清除画布
② 三层网格(同心正六边形) → 底层
③ 数据多边形填充 → 中层(半透明)
④ 数据多边形描边 → 上层(实线)
⑤ 顶点标签文字 → 最上层
5.2 叠加效果示意
最顶层 ⑤ 标签文字
④ 数据多边形描边(线宽 2)
③ 数据多边形填充(半透明 28%)
② 三层网格描边(线宽 1)
最底层 ① 画布清除(透明背景)
六、动画与数据多边形的结合
6.1 从中心向外扩散
// 通过 this.drawProgress(0→1)控制数据多边形的"半径比例"
const r = radius * (p.value / 100) * this.drawProgress
// drawProgress=0 时 → 所有顶点都汇聚到中心
// drawProgress=0.5 → 数据多边形绘制到 50%
// drawProgress=1 → 数据多边形到达最终位置
6.2 网格与数据多边形的动画独立
// 网格:直接绘制(不依赖 drawProgress)
for (let layer = 1; layer <= 3; layer++) {
const r = radius * (layer / 3)
// ... 直接绘制完整网格
}
// 数据多边形:依赖 drawProgress(从中心向外生长)
this.data.forEach((p, i) => {
const r = radius * (p.value / 100) * this.drawProgress
// ...
})
为什么网格不做动画?
- 网格是参照系,需要一开始就完整显示
- 数据多边形是数据展示,通过扩散动画吸引视觉焦点
七、样式调整参数
7.1 项目中可调样式参数
| 参数 | 当前值 | 效果 | 调整建议 |
|---|---|---|---|
| 填充透明度 | 0.28 | 淡淡显色 | 调高→更深;调低→更透 |
| 描边宽度 | 2px | 清晰可见 | 调高→更突出 |
| 描边颜色 | PRIMARY #A8907A | 品牌棕 | 可按情绪类型变化 |
7.2 不同情绪类型的数据多边形配色
/**
* 根据人格类型选择不同的数据多边形颜色
*/
function getRadarColor(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 颜色完全不透明,盖住了 stroke
ctx.fillStyle = '#A8907A' // ❌ 完全不透明
ctx.fill()
ctx.stroke() // stroke 被 fill 盖住
// 原因:fill 覆盖了 stroke 的区域
// ✅ 解决:使用半透明 fill,或先 stroke 再 fill
ctx.fillStyle = 'rgba(168,144,122,.28)' // 半透明
ctx.fill()
ctx.stroke() // 可见
8.2 数据多边形超出网格?
// 场景:某维度 value > 100,导致顶点超出最外层网格
// 原因:输入数据未校验
const safeValue = Math.min(p.value, 100) // 限制最大 100
const r = radius * (safeValue / 100)
8.3 数据点少于 6 个?
// 场景:只传入 4 个数据点
// 结果:绘制出四边形,而非六边形
// 解决:统一使用 6 个维度,不足时补 0
const paddedData = [...this.data]
while (paddedData.length < 6) {
paddedData.push({ label: '', value: 0 })
}
十、最佳实践与性能优化
10.1 代码组织规范
以下规范可显著提升代码可读性与可维护性:
- 分层解耦:UI 逻辑(pages/)与数据操作(DAO/Service)严格分离,不在 build() 中调用数据库
- 组件拆分:单个
@Component保持 100~200 行以内,大型组件拆分为@Builder子函数 - 令牌约束:颜色、字号、间距统一引用
AppColors/AppFonts/AppAnimations,禁止魔数 - 错误处理:异步方法统一返回
Promise<T | null>,异常时hilog.error记录并返回null
10.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 变量控制绘制进度:
// 带动画的数据多边形绘制
private drawDataPolygon(ctx: CanvasRenderingContext2D, scores: number[], progress: number): void {
const sides = 6
const radius = this.canvasSize / 2 * 0.75
ctx.save()
ctx.translate(this.canvasSize / 2, this.canvasSize / 2)
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 2
// progress 从 0 到 1,控制多边形"展开"动画
const r = (scores[i] / 100) * radius * progress
const x = r * Math.cos(angle)
const y = r * Math.sin(angle)
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
}
ctx.closePath()
ctx.fillStyle = 'rgba(139, 92, 246, 0.3)' // 品牌紫色半透明
ctx.fill()
ctx.strokeStyle = 'rgba(139, 92, 246, 0.9)'
ctx.lineWidth = 2
ctx.stroke()
ctx.restore()
}
7.1 绘制进度驱动的动画
@State @Watch('onProgressChange') drawProgress: number = 0
onProgressChange(): void {
this.draw() // 进度变化触发重绘
}
aboutToAppear(): void {
animateTo({ duration: 800, curve: Curve.EaseOut }, () => {
this.drawProgress = 1.0
})
}
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)