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 — 标签绘制部分constfontSize=Math.max(8,Math.round(11*this.radarSize/280))ctx.font=`${fontSize}px HarmonyOS Sans SC`ctx.fillStyle=AppColors.TEXT_2ctx.textAlign='center'ctx.textBaseline='middle'constlabelOffset=radius+Math.max(12,Math.round(18*this.radarSize/280))this.data.forEach((p,i)=>{constx=cx+labelOffset*Math.cos(angles[i])consty=cy+labelOffset*Math.sin(angles[i])ctx.fillText(p.label,x,y)})1.2 视觉效果
标签①(能量) ↑ │ ←──○──→ 标签④(社交) │ ↓ 标签③(情绪) 每个标签位于顶点向外延伸的方向上,距离顶点约 12-30px二、自适应字体大小
2.1 按比例缩放
constfontSize=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 偏移距离
constlabelOffset=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)=>{constx=cx+labelOffset*Math.cos(angles[i])consty=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组合,需要为每个顶点单独计算对齐方式:
// ❌ 如果每个方向单独处理,代码会变得复杂constalignMap=[{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°,标签可能互相覆盖// 方案:动态调整外推距离或缩小字号constminGap=Math.sin(Math.PI/sides)*labelOffset*2if(minGap<fontSize*2){// 空间不足,缩小字号或增大偏移}5.2 基于方向的偏移微调(扩展方案)
如果需要更精细的标签布局,可以按方向微调:
functiongetLabelOffset(i:number,sides:number,radius:number,fontSize:number):number{constbase=radius+18// 水平方向(左右)需要更多空间,因为文字是横向排列的constisHorizontal=i===1||i===4// 2 点和 8 点方向if(isHorizontal)returnbase+fontSize*0.8returnbase}六、字体选择
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 自适应对齐算法
privatedrawLabels(ctx:CanvasRenderingContext2D,cx:number,cy:number,r:number):void{constlabels=['能量','思维','行动','情绪','社交','细节']constlabelOffset=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)=>{constangle=(Math.PI/3)*i-Math.PI/2constx=r*labelOffset*Math.cos(angle)consty=r*labelOffset*Math.sin(angle)// 根据角度动态调整文字对齐constcosVal=Math.cos(angle)if(Math.abs(cosVal)<0.1){ctx.textAlign='center'// 顶部/底部标签居中}elseif(cosVal>0){ctx.textAlign='left'// 右侧标签左对齐}else{ctx.textAlign='right'// 左侧标签右对齐}// 根据 sin 值调整垂直基线constsinVal=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检查文字宽度并自适应字号:
// 自适应字号:超出宽度则缩小constmaxLabelWidth=r*0.4// 允许标签最大宽度letfontSize=12ctx.font=`${fontSize}px HarmonyOS Sans SC`consttextWidth=ctx.measureText(label).widthif(textWidth>maxLabelWidth){fontSize=Math.floor(fontSize*maxLabelWidth/textWidth)ctx.font=`${fontSize}px HarmonyOS Sans SC`}ctx.fillText(label,x,y)如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!