47 雷达图绘制:标签自适应布局
47 雷达图绘制:标签自适应布局
前言

图:47 雷达图绘制:标签自适应布局 运行效果截图(HarmonyOS NEXT)
雷达图的 6 个顶点各有一个维度标签(能量/思维/行动/情绪/社交/细节),这些标签需要刚好位于顶点外侧,并且在不同尺寸的雷达图上都能清晰展示。
本文以 [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)

图:雷达图6个顶点标签的自适应布局——根据角度位置动态调整对齐方式
一、标签绘制基础
1.1 核心代码
// HandwritingRadar.ets — 标签绘制部分
const fontSize = Math.max(8, Math.round(11 * this.radarSize / 280))
ctx.font = `${fontSize}px HarmonyOS Sans SC`
ctx.fillStyle = AppColors.TEXT_2
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const labelOffset = radius + Math.max(12, Math.round(18 * this.radarSize / 280))
this.data.forEach((p, i) => {
const x = cx + labelOffset * Math.cos(angles[i])
const y = cy + labelOffset * Math.sin(angles[i])
ctx.fillText(p.label, x, y)
})
1.2 视觉效果
标签①(能量)
↑
│
←──○──→ 标签④(社交)
│
↓
标签③(情绪)
每个标签位于顶点向外延伸的方向上,距离顶点约 12-30px
二、自适应字体大小
2.1 按比例缩放
const fontSize = Math.max(8, Math.round(11 * this.radarSize / 280))
计算逻辑:
| radarSize | 计算过程 | fontSize |
|---|---|---|
| 140(小) | 11 × 140 / 280 = 5.5 → Math.max(8, 6) |
8px(下限保护) |
| 200(中) | 11 × 200 / 280 = 7.86 → Math.max(8, 8) |
8px |
| 280(默认) | 11 × 280 / 280 = 11 → Math.max(8, 11) |
11px |
| 360(大) | 11 × 360 / 280 = 14.14 → Math.max(8, 14) |
14px |
设计要点:
- 基准大小:
280px画布对应11px字号 - 线性缩放:字号随画布大小线性变化
- 下限保护:
Math.max(8, ...)确保最小 8px,避免太小无法阅读
2.2 字号下限保护的原因
// ❌ 如果没有 Math.max(8, ...)
fontSize = 11 * 100 / 280 ≈ 4 // 4px 完全无法阅读
// ✅ 有 Math.max(8, ...) 保护
fontSize = Math.max(8, 4) = 8 // 8px 勉强可读
三、自适应标签偏移
3.1 偏移距离
const labelOffset = radius + Math.max(12, Math.round(18 * this.radarSize / 280))
标签位置 = 网格顶点 + 外推偏移,确保标签不盖住网格:
网格顶点(radius)
───┬───
│
↓ ← labelOffset = 顶点 + 外推距离
文字标签
偏移计算:
| radarSize | 外推距离 | 公式 | labelOffset |
|---|---|---|---|
| 140 | 12px(下限) | radius + 12 |
radius + 12 |
| 200 | 13px | radius + 13 |
radius + 13 |
| 280 | 18px | radius + 18 |
radius + 18 |
| 360 | 23px | radius + 23 |
radius + 23 |
3.2 偏移方向
每个标签沿着该顶点方向的射线外推:
this.data.forEach((p, i) => {
const x = cx + labelOffset * Math.cos(angles[i])
const y = cy + labelOffset * Math.sin(angles[i])
// angles[i] 与网格顶点计算使用的角度相同
// 所以标签位于顶点正外方
})
方向对照:
| 标签 | 角度 | 方向 | 标签位置 |
|---|---|---|---|
| 能量 | -90° | ↑ | 正上方 |
| 思维 | -30° | ↗ | 右上 |
| 行动 | 30° | ↘ | 右下 |
| 情绪 | 90° | ↓ | 正下方 |
| 社交 | 150° | ↙ | 左下 |
| 细节 | -150° | ↖ | 左上 |
四、文字对齐设置
4.1 textAlign 与 textBaseline
ctx.textAlign = 'center' // 水平居中
ctx.textBaseline = 'middle' // 垂直居中
textAlign 可选值:
| 值 | 效果 | 适用场景 |
|---|---|---|
left |
从左向右排列 | 正左标签(如 9 点钟方向) |
center |
以(x,y)为中心 | ✅ 本项目:所有标签居中于顶点方向 |
right |
从右向左排列 | 正右标签(如 3 点钟方向) |
textBaseline 可选值:
| 值 | 效果 | 适用场景 |
|---|---|---|
top |
顶部对齐 | 标签在点下方 |
middle |
垂直居中 | ✅ 本项目:所有标签居中 |
bottom |
底部对齐 | 标签在点上方 |
alphabetic |
字母基线 | 英文文本 |
4.2 为什么统一 center + middle?
对于分布在 6 个方向的标签,如果使用 left/right/top/bottom 组合,需要为每个顶点单独计算对齐方式:
// ❌ 如果每个方向单独处理,代码会变得复杂
const alignMap = [
{ align: 'center', baseline: 'bottom' }, // 12点方向:文字在点下方
{ align: 'left', baseline: 'middle' }, // 2点方向:文字在点右侧
{ align: 'left', baseline: 'top' }, // 4点方向:...
{ align: 'center', baseline: 'top' }, // 6点方向
{ align: 'right', baseline: 'top' }, // 8点方向
{ align: 'right', baseline: 'middle' }, // 10点方向
]
// ✅ 本项目:统一 center + middle + labelOffset 外推
// 所有标签从顶点向外推,自然分布在顶点外侧
// 代码简洁,效果一致
五、标签碰撞处理
5.1 当前方案 vs 碰撞检测
当前方案将所有标签沿射线外推相同距离,在标准雷达图(6 个维度)下不会出现重叠,因为:
- 6 个顶点间隔 60°,间距足够
- 标签文字较短(2 个汉字)
- 外推距离(18px)保证标签在网格外部
可能重叠的极端情况:
// 场景:大量维度(如 12 维雷达图)
// 顶点间距只有 30°,标签可能互相覆盖
// 方案:动态调整外推距离或缩小字号
const minGap = Math.sin(Math.PI / sides) * labelOffset * 2
if (minGap < fontSize * 2) {
// 空间不足,缩小字号或增大偏移
}
5.2 基于方向的偏移微调(扩展方案)
如果需要更精细的标签布局,可以按方向微调:
function getLabelOffset(i: number, sides: number, radius: number, fontSize: number): number {
const base = radius + 18
// 水平方向(左右)需要更多空间,因为文字是横向排列的
const isHorizontal = i === 1 || i === 4 // 2 点和 8 点方向
if (isHorizontal) return base + fontSize * 0.8
return base
}
六、字体选择
6.1 字体族
ctx.font = `${fontSize}px HarmonyOS Sans SC`
项目使用系统字体 HarmonyOS Sans SC,这是鸿蒙系统的默认无衬线字体。
可用的系统字体:
| 字体 | 风格 | 用途 |
|---|---|---|
HarmonyOS Sans SC |
无衬线 | UI 文本、标签(✅ 本项目使用) |
HarmonyOS Sans SC Medium |
中等字重 | 标题、强调 |
HarmonyOS Sans SC Bold |
粗体 | 数字、数值 |
6.2 完整字体设置
// Canvas 中设置字体比 ArkUI Text 组件更灵活
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`
// 参数:字重 + 字号 + 字体族(字符串拼接)
// 示例
ctx.font = '11px HarmonyOS Sans SC' // 普通
ctx.font = 'bold 36px HarmonyOS Sans SC' // 粗体大号(MatchRing 中心数值)
ctx.font = '12px HarmonyOS Sans SC' // 中等(MatchRing 副标题)
七、不同场景的标签配置
7.1 标签与数据显示
项目中的标签有 6 个固定维度,每个维度垂直对应的数据值也在标签附近展示(在页面 UI 层而非 Canvas 层实现):
Canvas 层:
能量
↑
│
细节 ↖ ○ ↗ 思维
社交 ← ○ → 行动
↓
情绪
页面 UI 层:
[能量 62] [思维 65] [行动 55]
[情绪 72] [社交 60] [细节 70]
7.2 小尺寸雷达图的标签处理
当雷达图用作小尺寸预览时(如 SharePage 中 radarSize=200),标签字号自动缩小:
// SharePage 中使用小雷达
HandwritingRadar({
data: this.radarData,
radarSize: 200 // 比默认 280 小 29%
})
// 字号:Math.max(8, 11*200/280) = Math.max(8, 8) = 8px
// 偏移:radius + Math.max(12, 18*200/280) = radius + 13
八、标签布局检查清单
| 检查项 | 项目中当前值 | 是否满足 |
|---|---|---|
| 字号自适应 | 11 × size/280 最小 8px |
✅ |
| 标签位置 | 顶点外推 Math.max(12, 18×size/280) |
✅ |
| 文字对齐 | center + middle | ✅ |
| 抗锯齿 | RenderingContextSettings antialias=true | ✅ |
| 标签不重叠 | 6 维度 60° 间隔 + 短文字 | ✅ |
| 颜色可见 | fillStyle = TEXT_2 (深灰色) |
✅ |
| 字体加载 | 使用系统内置 HarmonyOS Sans SC | ✅ |
总结
本文详细解析了雷达图标签的自适应布局方案:
- 自适应字号:
Math.max(8, 11 × radarSize / 280)线性缩放 + 下限保护 - 自适应偏移:
radius + Math.max(12, 18 × radarSize / 280)外推顶点 - 对齐方式:统一
center+middle简化代码 - 方向计算:复用网格顶点的
angles[],标签位于顶点正外方 - 极端处理:小尺寸下字号保护 + 外推距离缩小
下一篇文章将介绍 EmotionTrendChart 情绪折线图——30天情绪趋势的 Canvas 绘制。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- Canvas fillText / textAlign / textBaseline
- [HandwritingRadar 标签源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)
- HarmonyOS Sans SC 字体
- Canvas font 属性
- [SharePage 小雷达图使用](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/SharePage.ets)
- [AppFonts 字体常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppFonts.ets)
- HarmonyOS 开发文档
七、标签绘制完整实现
7.1 自适应对齐算法
private drawLabels(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number): void {
const labels = ['能量', '思维', '行动', '情绪', '社交', '细节']
const labelOffset = 1.25 // 标签距中心的倍数
ctx.save()
ctx.translate(cx, cy)
ctx.font = '12px HarmonyOS Sans SC'
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'
labels.forEach((label, i) => {
const angle = (Math.PI / 3) * i - Math.PI / 2
const x = r * labelOffset * Math.cos(angle)
const y = r * labelOffset * Math.sin(angle)
// 根据角度动态调整文字对齐
const cosVal = Math.cos(angle)
if (Math.abs(cosVal) < 0.1) {
ctx.textAlign = 'center' // 顶部/底部标签居中
} else if (cosVal > 0) {
ctx.textAlign = 'left' // 右侧标签左对齐
} else {
ctx.textAlign = 'right' // 左侧标签右对齐
}
// 根据 sin 值调整垂直基线
const sinVal = Math.sin(angle)
ctx.textBaseline = sinVal > 0.5 ? 'top' : (sinVal < -0.5 ? 'bottom' : 'middle')
ctx.fillText(label, x, y)
})
ctx.restore()
}
7.2 标签样式参数对照表
| 参数 | 项目值 | 效果 |
|---|---|---|
font |
'12px HarmonyOS Sans SC' |
小字体,与图表协调 |
fillStyle |
rgba(255,255,255,0.85) |
白色微透明,图表背景对比 |
textAlign |
动态(left/center/right) | 自适应位置 |
textBaseline |
动态(top/middle/bottom) | 垂直方向居中 |
labelOffset |
1.25 | 标签在顶点外侧 25% 处 |
7.3 多语言适配注意事项
当标签文本较长(如英文标签)时,需要用 measureText 检查文字宽度并自适应字号:
// 自适应字号:超出宽度则缩小
const maxLabelWidth = r * 0.4 // 允许标签最大宽度
let fontSize = 12
ctx.font = `${fontSize}px HarmonyOS Sans SC`
const textWidth = ctx.measureText(label).width
if (textWidth > maxLabelWidth) {
fontSize = Math.floor(fontSize * maxLabelWidth / textWidth)
ctx.font = `${fontSize}px HarmonyOS Sans SC`
}
ctx.fillText(label, x, y)
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐



所有评论(0)