47 雷达图绘制标签自适应布局前言图47 雷达图绘制标签自适应布局 运行效果截图HarmonyOS NEXT雷达图的 6 个顶点各有一个维度标签能量/思维/行动/情绪/社交/细节这些标签需要刚好位于顶点外侧并且在不同尺寸的雷达图上都能清晰展示。本文以 [HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets) 中的标签绘制代码为例深入解析 Canvas 文字绘制的自适应布局方案。鸿蒙官方·Canvas 文字 APIdeveloper.huawei.com项目源码[HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)图雷达图6个顶点标签的自适应布局——根据角度位置动态调整对齐方式左侧 180°-360°右侧 0°-180°顶部 270°附近底部 90°附近6个标签计算标签中心坐标顶点坐标 × labelOffset角度区间textAlign righttextAlign lefttextAlign centerfillText 绘制一、标签绘制基础1.1 核心代码// HandwritingRadar.ets — 标签绘制部分constfontSizeMath.max(8,Math.round(11*this.radarSize/280))ctx.font${fontSize}px HarmonyOS Sans SCctx.fillStyleAppColors.TEXT_2ctx.textAligncenterctx.textBaselinemiddleconstlabelOffsetradiusMath.max(12,Math.round(18*this.radarSize/280))this.data.forEach((p,i){constxcxlabelOffset*Math.cos(angles[i])constycylabelOffset*Math.sin(angles[i])ctx.fillText(p.label,x,y)})1.2 视觉效果标签①能量 ↑ │ ←──○──→ 标签④社交 │ ↓ 标签③情绪 每个标签位于顶点向外延伸的方向上距离顶点约 12-30px二、自适应字体大小2.1 按比例缩放constfontSizeMath.max(8,Math.round(11*this.radarSize/280))计算逻辑radarSize计算过程fontSize140小11 × 140 / 280 5.5→Math.max(8, 6)8px下限保护200中11 × 200 / 280 7.86→Math.max(8, 8)8px280默认11 × 280 / 280 11→Math.max(8, 11)11px360大11 × 360 / 280 14.14→Math.max(8, 14)14px设计要点基准大小280px画布对应11px字号线性缩放字号随画布大小线性变化下限保护Math.max(8, ...)确保最小 8px避免太小无法阅读2.2 字号下限保护的原因// ❌ 如果没有 Math.max(8, ...)fontSize11*100/280≈4// 4px 完全无法阅读// ✅ 有 Math.max(8, ...) 保护fontSizeMath.max(8,4)8// 8px 勉强可读三、自适应标签偏移3.1 偏移距离constlabelOffsetradiusMath.max(12,Math.round(18*this.radarSize/280))标签位置 网格顶点 外推偏移确保标签不盖住网格网格顶点radius ───┬─── │ ↓ ← labelOffset 顶点 外推距离 文字标签偏移计算radarSize外推距离公式labelOffset14012px下限radius 12radius 1220013pxradius 13radius 1328018pxradius 18radius 1836023pxradius 23radius 233.2 偏移方向每个标签沿着该顶点方向的射线外推this.data.forEach((p,i){constxcxlabelOffset*Math.cos(angles[i])constycylabelOffset*Math.sin(angles[i])// angles[i] 与网格顶点计算使用的角度相同// 所以标签位于顶点正外方})方向对照标签角度方向标签位置能量-90°↑正上方思维-30°↗右上行动30°↘右下情绪90°↓正下方社交150°↙左下细节-150°↖左上四、文字对齐设置4.1 textAlign 与 textBaselinectx.textAligncenter// 水平居中ctx.textBaselinemiddle// 垂直居中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°标签可能互相覆盖// 方案动态调整外推距离或缩小字号constminGapMath.sin(Math.PI/sides)*labelOffset*2if(minGapfontSize*2){// 空间不足缩小字号或增大偏移}5.2 基于方向的偏移微调扩展方案如果需要更精细的标签布局可以按方向微调functiongetLabelOffset(i:number,sides:number,radius:number,fontSize:number):number{constbaseradius18// 水平方向左右需要更多空间因为文字是横向排列的constisHorizontali1||i4// 2 点和 8 点方向if(isHorizontal)returnbasefontSize*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.font11px HarmonyOS Sans SC// 普通ctx.fontbold 36px HarmonyOS Sans SC// 粗体大号MatchRing 中心数值ctx.font12px HarmonyOS Sans SC// 中等MatchRing 副标题七、不同场景的标签配置7.1 标签与数据显示项目中的标签有 6 个固定维度每个维度垂直对应的数据值也在标签附近展示在页面 UI 层而非 Canvas 层实现Canvas 层 能量 ↑ │ 细节 ↖ ○ ↗ 思维 社交 ← ○ → 行动 ↓ 情绪 页面 UI 层 [能量 62] [思维 65] [行动 55] [情绪 72] [社交 60] [细节 70]7.2 小尺寸雷达图的标签处理当雷达图用作小尺寸预览时如 SharePage 中radarSize200标签字号自动缩小// 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 antialiastrue✅标签不重叠6 维度 60° 间隔 短文字✅颜色可见fillStyle TEXT_2(深灰色)✅字体加载使用系统内置 HarmonyOS Sans SC✅总结本文详细解析了雷达图标签的自适应布局方案自适应字号Math.max(8, 11 × radarSize / 280)线性缩放 下限保护自适应偏移radius Math.max(12, 18 × radarSize / 280)外推顶点对齐方式统一centermiddle简化代码方向计算复用网格顶点的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[能量,思维,行动,情绪,社交,细节]constlabelOffset1.25// 标签距中心的倍数ctx.save()ctx.translate(cx,cy)ctx.font12px HarmonyOS Sans SCctx.fillStylergba(255, 255, 255, 0.85)labels.forEach((label,i){constangle(Math.PI/3)*i-Math.PI/2constxr*labelOffset*Math.cos(angle)constyr*labelOffset*Math.sin(angle)// 根据角度动态调整文字对齐constcosValMath.cos(angle)if(Math.abs(cosVal)0.1){ctx.textAligncenter// 顶部/底部标签居中}elseif(cosVal0){ctx.textAlignleft// 右侧标签左对齐}else{ctx.textAlignright// 左侧标签右对齐}// 根据 sin 值调整垂直基线constsinValMath.sin(angle)ctx.textBaselinesinVal0.5?top:(sinVal-0.5?bottom:middle)ctx.fillText(label,x,y)})ctx.restore()}7.2 标签样式参数对照表参数项目值效果font12px HarmonyOS Sans SC小字体与图表协调fillStylergba(255,255,255,0.85)白色微透明图表背景对比textAlign动态left/center/right自适应位置textBaseline动态top/middle/bottom垂直方向居中labelOffset1.25标签在顶点外侧 25% 处7.3 多语言适配注意事项当标签文本较长如英文标签时需要用measureText检查文字宽度并自适应字号// 自适应字号超出宽度则缩小constmaxLabelWidthr*0.4// 允许标签最大宽度letfontSize12ctx.font${fontSize}px HarmonyOS Sans SCconsttextWidthctx.measureText(label).widthif(textWidthmaxLabelWidth){fontSizeMath.floor(fontSize*maxLabelWidth/textWidth)ctx.font${fontSize}px HarmonyOS Sans SC}ctx.fillText(label,x,y)如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力