45_雷达图正多边形网格生成
45 雷达图绘制:正多边形网格生成
前言

图:45 雷达图绘制:正多边形网格生成 运行效果截图(HarmonyOS NEXT)
雷达图(Radar Chart)是"鹿鹿·笔迹心理分析"App 中最核心的数据可视化组件。它用 6 个维度(能量/思维/行动/情绪/社交/细节)来展示一个人的人格特征,每个维度从 0 到 100 打分。
绘制雷达图的第一步,是画出三层正六边形网格背景。这是数据多边形的"坐标系"——用户通过网格的层数和刻度感知数值大小。
本文以 [HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets) 中的网格绘制代码为例,深入解析正多边形的坐标计算和 Canvas 绘制。
鸿蒙官方·Canvas 路径 API:developer.huawei.com
项目源码:[HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)

图:雷达图三层正六边形网格——顶点坐标计算与 Canvas 路径绘制
一、正多边形顶点坐标计算
1.1 核心公式
正六边形的 6 个顶点均匀分布在圆周上,每个顶点的坐标用极坐标→直角坐标转换计算:
// 第 i 个顶点的角度
angle = (2π × i) / 边数 - π/2
// 第 i 个顶点的坐标
x = 中心X + 半径 × cos(angle)
y = 中心Y + 半径 × sin(angle)
关键点:为什么减 π/2?
标准三角函数的 0 度指向 3 点钟方向,但雷达图通常要求第一个顶点指向上方(12 点钟方向),所以需要偏移 -π/2:
-π/2(12点,第一个顶点)
↑
│
←──○──→ 0(3点 标准角度)
│
↓
π/2(6点)
1.2 项目中实际代码
// HandwritingRadar.ets — 顶点角度预计算
const sides = this.data.length || 6 // 边数(与数据维度数相同)
const angles: number[] = []
for (let i = 0; i < sides; i++) {
// i=0 → -π/2(12点方向)
// i=1 → -π/2 + π/3 = -π/6(2点方向)
// i=2 → -π/2 + 2π/3 = π/6(4点方向)
// i=3 → -π/2 + π = π/2(6点方向)
// i=4 → -π/2 + 4π/3 = 5π/6(8点方向)
// i=5 → -π/2 + 5π/3 = -5π/6(10点方向)
angles.push((Math.PI * 2 * i) / sides - Math.PI / 2)
}
1.3 角度与顶点对照表
| 顶点 i | 角度(弧度) | 角度(度) | 方向 |
|---|---|---|---|
| 0 | -π/2 | -90° | ↑(12点) |
| 1 | -π/6 | -30° | ↗(2点) |
| 2 | π/6 | 30° | ↘(4点) |
| 3 | π/2 | 90° | ↓(6点) |
| 4 | 5π/6 | 150° | ↙(8点) |
| 5 | -5π/6 | -150° | ↖(10点) |
二、三层网格生成
2.1 同心多边形
网格由 3 个同心正六边形组成,每层半径按比例递增:
const radius = W * 0.34 // 最外层半径(占画布 34%)
const layers = 3 // 网格层数
for (let layer = 1; layer <= layers; layer++) {
const r = radius * (layer / 3) // 第 1 层 = 1/3 半径
// 第 2 层 = 2/3 半径
// 第 3 层 = 3/3 半径
// ... 绘制这一层的六边形
}
2.2 完整网格绘制
private draw(): void {
const ctx = this.context
const W = this.radarSize // 画布尺寸 280
const cx = W / 2 // 中心 X
const cy = W / 2 // 中心 Y
const radius = W * 0.34 // 最外层半径(≈95px)
// 预计算 6 个顶点角度
const sides = this.data.length || 6
const angles: number[] = []
for (let i = 0; i < sides; i++) {
angles.push((Math.PI * 2 * i) / sides - Math.PI / 2)
}
// 清除画布
ctx.clearRect(0, 0, W, W)
// === 绘制三层网格 ===
for (let layer = 1; layer <= 3; layer++) {
const r = radius * (layer / 3) // 当前层半径
ctx.beginPath() // 开始新路径
for (let i = 0; i < sides; i++) {
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.strokeStyle = AppColors.LINE // 颜色:#E5D5BC
ctx.lineWidth = 1
ctx.stroke() // 描边
}
}
绘制过程可视化:
第 3 层(最外层) ─── 半径 = radius × 3/3
/\
/ \
/ \
/______\
第 2 层(中层) ─── 半径 = radius × 2/3
/\
/ \
/____\
第 1 层(最内层) ─── 半径 = radius × 1/3
/\
/ \
/____\
三、三层网格含义
3.1 数值映射
网格的每一层代表一个数值边界:
// 第 1 层(最内层) → 数值 ≈ 33
// 第 2 层(中层) → 数值 ≈ 67
// 第 3 层(最外层) → 数值 = 100
//
// 因此,一个点的位置(在网格中的百分比位置)
// 决定了它的 6 维数值:
// 距中心 r / 半径 = 数值 / 100
3.2 用户视觉感知
三层网格的用户感知:
第 1 层:33 分线 — 低于此说明该维度较弱
第 2 层:67 分线 — 高于此说明该维度较强
第 3 层:边界线 — 满分 100 的外框
四、性能优化:角度预计算
4.1 避免重复计算
代码中将角度计算放在 draw() 的最前面,而不是在每次循环中重复计算:
// ✅ 高效:一次性预计算所有角度
const angles: number[] = []
for (let i = 0; i < sides; i++) {
angles.push((Math.PI * 2 * i) / sides - Math.PI / 2)
}
// 在网格循环中复用
for (let layer = 1; layer <= 3; layer++) {
const r = radius * (layer / 3)
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const x = cx + r * Math.cos(angles[i]) // 复用 angles
const y = cy + r * Math.sin(angles[i])
// ...
}
}
// 在数据多边形中使用同一个 angles 数组
this.data.forEach((p, i) => {
const r = radius * (p.value / 100) * this.drawProgress
const x = cx + r * Math.cos(angles[i]) // 复用 angles
const y = cy + r * Math.sin(angles[i])
// ...
})
4.2 避免的写法
// ❌ 低效:每次循环都重复计算角度(cos/sin 是 CPU 密集型运算)
for (let i = 0; i < sides; i++) {
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2
const x = cx + r * Math.cos(angle)
const y = cy + r * Math.sin(angle)
}
五、正多边形绘制总结
5.1 通用函数
/**
* 绘制正多边形
* @param ctx Canvas 2D 上下文
* @param cx 中心 X
* @param cy 中心 Y
* @param radius 外接圆半径
* @param sides 边数
* @param rotation 旋转偏移(弧度)
*/
function drawPolygon(
ctx: CanvasRenderingContext2D,
cx: number, cy: number,
radius: number,
sides: number,
rotation: number = -Math.PI / 2
): void {
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (Math.PI * 2 * i) / sides + rotation
const x = cx + radius * Math.cos(angle)
const y = cy + radius * Math.sin(angle)
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.closePath()
}
5.2 不同边数的雷达图
// 6 维雷达图(本项目使用)
drawPolygon(ctx, cx, cy, radius, 6)
// 5 维雷达图(游戏角色面板)
drawPolygon(ctx, cx, cy, radius, 5)
// 8 维雷达图(综合能力评估)
drawPolygon(ctx, cx, cy, radius, 8)
| 边数 | 常见用途 | 示例 |
|---|---|---|
| 5 | 游戏角色属性 | 攻击/防御/速度/法力/生命 |
| 6 | 💡 本项目人格分析 | 能量/思维/行动/情绪/社交/细节 |
| 8 | 综合能力评估 | 沟通/决策/创新/执行/领导/协作/学习/适应 |
5.3 步进式添加顶点连线(可选扩展)
除了同心多边形,还可以绘制从中心到顶点的辅助线:
// 从中心到各顶点的辐射线
for (let i = 0; i < sides; i++) {
const x = cx + radius * Math.cos(angles[i])
const y = cy + radius * Math.sin(angles[i])
ctx.beginPath()
ctx.moveTo(cx, cy)
ctx.lineTo(x, y)
ctx.strokeStyle = AppColors.LINE
ctx.lineWidth = 0.5
ctx.stroke()
}
六、常见问题
6.1 画出来的是椭圆形?
// ❌ 原因:Canvas 宽高不等时,正圆形被拉伸
Canvas(this.context)
.width(300).height(200) // 宽高比 3:2 → 六边形被拉伸
// ✅ 解决:使用正方形 Canvas
Canvas(this.context)
.width(280).height(280) // 宽高相等
6.2 网格显示不完整?
// ❌ 原因:半径太大,超出画布边界
const radius = W * 0.6 // 60% — 顶点接近边界被裁剪
// ✅ 解决:留出标签空间
const radius = W * 0.34 // 34% — 只占画布中心区域
// 余下空间留给顶点标签
6.3 路径未闭合?
// ❌ 错误:缺少 closePath,最后一个顶点没有回到起点
ctx.beginPath()
ctx.moveTo(x0, y0)
ctx.lineTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke() // 只画了两条边
// ✅ 正确:closePath 自动闭合
ctx.closePath()
ctx.stroke() // 完整的六边形
八、注意事项与常见问题
8.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期:合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
总结
本文详细解析了雷达图三层正六边形网格的生成:
- 顶点坐标计算:
(2π × i / sides) - π/2+ cos/sin 转换 - 三层同心网格:
for loop递进半径(1/3 → 2/3 → 3/3) - 路径绘制流程:
beginPath → moveTo → lineTo × 5 → closePath → stroke - 性能优化:角度预计算避免重复 cos/sin
- 网格含义:每层对应数值 33/67/100 分界线
下一篇文章将介绍 数据多边形与填充——如何在网格上绘制彩色数据区域。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- CanvasRenderingContext2D 路径方法
- [HandwritingRadar 网格绘制源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)
- 正多边形顶点坐标公式
- [AppColors 颜色常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppColors.ets)
- ArkUI Canvas 绘图指南
- HarmonyOS 开发文档
七、完整绘制流程总结
雷达图网格绘制的完整步骤:
| 步骤 | 操作 | Canvas API |
|---|---|---|
| 1. 保存状态 | 保存上下文变换矩阵 | ctx.save() |
| 2. 平移到中心 | 将坐标原点移到画布中心 | ctx.translate(cx, cy) |
| 3. 绘制三层网格 | 循环 layer = 0.3/0.6/1.0 | 嵌套循环 |
| 4. 计算顶点 | 三角函数计算 6 个顶点坐标 | Math.cos/sin |
| 5. 绘制路径 | 连接顶点闭合为多边形 | moveTo/lineTo/closePath |
| 6. 绘制轴线 | 从中心到顶点的 6 条轴 | moveTo(0,0)/lineTo |
| 7. 恢复状态 | 还原变换矩阵 | ctx.restore() |
// 雷达图网格绘制核心代码
private drawGrid(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number): void {
const sides = 6
const layers = [0.3, 0.6, 1.0]
ctx.save()
ctx.translate(cx, cy)
layers.forEach(ratio => {
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 2 // 从顶部开始
const x = r * ratio * Math.cos(angle)
const y = r * ratio * Math.sin(angle)
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
}
ctx.closePath()
ctx.strokeStyle = `rgba(255,255,255,${0.15 + ratio * 0.1})`
ctx.stroke()
})
ctx.restore()
}
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐




所有评论(0)