当前位置: 首页 > news >正文

画个饼,给数据点颜色看看——在 HarmonyOS 模拟器上手搓一个饼图/环形图组件

前言

最近接了个小活,要在一份报告中展示用户来源分布。甲方给了个 Excel 表格,里面有七组数据,每个数据配一个标签。我盯着那堆数字看了半天,忽然意识到他们真正想要的不是表格,是一张饼图——大块儿的“自然流量”一眼就能看出来,小块的“付费广告”也不会被忽略。饼图这东西,就是给数字穿上戏服,让它们在一张圆饼上争地盘。

市面上画图表的库不少,但 HarmonyOS 应用里,我更喜欢自己用 Canvas 画。一方面不用多引入一个依赖,另一方面画饼图、环形图这种简单的几何图形,本身就几段扇形加几行文字,没比调用库复杂多少,还能精确控制每一像素的样子。于是上周某天晚上,我打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上,从零手写了一个饼图/环形图切换工具。它接收一个 JSON 数组,自动算百分比,给每个扇区分配颜色,在 Canvas 上画出扇区,并在旁边标上文字和图例。这篇文章就是那个晚上的记录,里面有弧线怎么画、百分比怎么转成角度、环形图怎么在饼图上挖个洞,以及怎么让图例乖乖排成一行。代码也给全,你拷进模拟器,换上自己的数据就能用。

一、一个扇形是怎么在 Canvas 上“切”出来的

饼图的每个扇区,其实就是一个圆的一部分——数学上叫“扇形”。在 HarmonyOS 的 Canvas 里,画扇形用的是arc方法,配合moveTolineTo。核心思路是:从圆心出发,先moveTo到圆心,然后画一条弧,再lineTo回到圆心,闭合路径,填色,描边。

ctx.arc(cx, cy, radius, startAngle, endAngle, counterclockwise)接受六个参数:圆心坐标、半径、起始角度、终止角度、是否逆时针。角度单位是弧度,0 弧度在三点钟方向,顺时针方向为正。我们习惯从 12 点钟方向开始画饼图(也就是 -π/2 的位置),所以每个扇区的起始角度就是上一个扇区的终止角度,初始值设为-Math.PI / 2

给定一组数据,如何算出每个扇区占多少角度?首先把所有数据的value加起来得到总数,然后每个数据的角度占比就是(value / total) * 2π。这样我们有了一个角度数组,遍历它,一边画扇形,一边累加起始角度。

画扇形的关键代码如下:

let startAngle = -Math.PI / 2; for (let item of data) { let sweepAngle = (item.value / total) * 2 * Math.PI; let endAngle = startAngle + sweepAngle; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = item.color; ctx.fill(); ctx.stroke(); startAngle = endAngle; }

ctx.arc画出的弧是圆周长的一部分,moveTo到圆心,再closePath,自然就从弧的终点连回了圆心,形成一个完整的扇形。整个过程非常直观,就像用圆规画一个角,然后把角的边涂上颜色。

二、把饼图挖个洞——环形图的绘制技巧

环形图就是饼图中间多了一个空白圆洞。看上去复杂,实际上只需在画扇区时,把“从圆心出发”改为“从内圆出发”。也就是说,我们画的不是一个从圆心开始的扇形,而是一个“扇环”——内半径和外半径之间的一圈厚片。

Canvas 没有直接画扇环的方法,但我们可以用路径拼接:先用arc画外弧,再从外弧终点画线到内弧终点(实际上不是直线,是径向线),然后用arc逆时针画内弧,再画线回到起点。这个路径用moveToarc组合起来就能实现。

具体做法是:

  1. 从内圆的扇区起点开始:moveTo(innerStartX, innerStartY)
  2. 画外弧:arc(cx, cy, outerRadius, startAngle, endAngle)
  3. 画线到内圆终点:lineTo(innerEndX, innerEndY)
  4. 画内弧逆时针:arc(cx, cy, innerRadius, endAngle, startAngle, true)—— 注意这里true表示逆时针,因为我们要反向画回起点。
  5. 闭合路径。

代码示例:

ctx.beginPath(); ctx.arc(cx, cy, outerRadius, startAngle, endAngle); ctx.arc(cx, cy, innerRadius, endAngle, startAngle, true); ctx.closePath(); ctx.fill();

这种双弧线画法在 Canvas 中非常常用,画环形图、进度环都用同一套逻辑。画出来的环形扇区,内圆是空的,正好可以用来放一个总数值或者标题,让图表更有“仪表盘”的感觉。

三、标签和图例——让饼图开口说话

光有颜色还不够,需要告诉观众每个扇区代表什么。这就需要标签和图例。

标签:一般放在扇区内部或旁边。放在内部时,我们可以计算扇区中间角度的位置,然后从圆心向外延伸一定距离(比如半径的 60% 处),画上文字。为了让文字居中,可以用ctx.textAlign = 'center'ctx.textBaseline = 'middle'。如果扇区太小,文字可能会挤在一起,那就省略不画。

图例:通常在图表下方或右侧,用一个小色块加上文字说明。在我们的实现里,Canvas 下方留出一块区域,循环绘制色块矩形和标签文字。排列方式可以横向排成几行,自动换行。为了方便,我给每个图例项固定宽度,用fillRect画色块,fillText画文字。

图例的数据和颜色直接从原始数据数组里拿。这样,扇区和图例共用同一组颜色,确保对应关系清晰。

另一个小细节:颜色分配。为了让饼图好看,我预设了一组颜色数组,比如['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#E7E9ED']。数据项数超过数组长度时,可以循环使用。颜色用 HSL 生成也能保证视觉差异,但这里为了简单直接用预设值。

四、完整代码——画布上的饼图/环形图自由切换

以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,替换entry/src/main/ets/pages/Index.ets。无需任何权限。

/* * 饼图与环形图绘制 * 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22 */ import { CanvasRenderingContext2D } from '@ohos.graphics.canvas'; interface ChartData { label: string; value: number; color: string; } const COLORS: string[] = [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#E7E9ED' ]; @Entry @Component struct Index { @State isRing: boolean = false; // 是否环形图 @State data: ChartData[] = []; private ctx: CanvasRenderingContext2D | null = null; private canvasWidth: number = 0; private canvasHeight: number = 0; async aboutToAppear(): Promise<void> { // 初始化示例数据 this.data = [ { label: '自然流量', value: 335, color: COLORS[0] }, { label: '付费广告', value: 210, color: COLORS[1] }, { label: '社交媒体', value: 190, color: COLORS[2] }, { label: '邮件营销', value: 140, color: COLORS[3] }, { label: '直接访问', value: 125, color: COLORS[4] } ]; } private onCanvasReady(ctx: CanvasRenderingContext2D): void { this.ctx = ctx; this.canvasWidth = ctx.canvas.width; this.canvasHeight = ctx.canvas.height; this.drawChart(); } private drawChart(): void { if (!this.ctx) return; let ctx = this.ctx; let w = this.canvasWidth; let h = this.canvasHeight; ctx.clearRect(0, 0, w, h); // 背景 ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, w, h); // 计算总和 let total = 0; for (let item of this.data) { total += item.value; } if (total === 0) return; // 饼图/环形图中心与半径 let cx = w / 2; let cy = h * 0.35; // 上方留给图例 let outerRadius = Math.min(w, h) * 0.3; let innerRadius = this.isRing ? outerRadius * 0.5 : 0; // 绘制扇区 let startAngle = -Math.PI / 2; // 从12点方向开始 for (let item of this.data) { let sweepAngle = (item.value / total) * 2 * Math.PI; let endAngle = startAngle + sweepAngle; ctx.beginPath(); if (innerRadius > 0) { // 环形扇区 ctx.arc(cx, cy, outerRadius, startAngle, endAngle); ctx.arc(cx, cy, innerRadius, endAngle, startAngle, true); ctx.closePath(); } else { // 饼图扇区 ctx.moveTo(cx, cy); ctx.arc(cx, cy, outerRadius, startAngle, endAngle); ctx.closePath(); } ctx.fillStyle = item.color; ctx.fill(); ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2; ctx.stroke(); // 绘制标签(在扇区中心) let midAngle = startAngle + sweepAngle / 2; let labelRadius = outerRadius * 0.65; let labelX = cx + labelRadius * Math.cos(midAngle); let labelY = cy + labelRadius * Math.sin(midAngle); // 仅当扇区足够大时绘制 let percent = Math.round((item.value / total) * 100); if (percent >= 5) { ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(`${percent}%`, labelX, labelY); } startAngle = endAngle; } // 绘制图例 let legendX = 40; let legendY = h * 0.7; let itemHeight = 25; let itemWidth = 120; ctx.font = '13px sans-serif'; ctx.textAlign = 'start'; ctx.textBaseline = 'middle'; for (let i = 0; i < this.data.length; i++) { let x = legendX + (i % 3) * (itemWidth + 20); // 每行3个 let y = legendY + Math.floor(i / 3) * itemHeight; ctx.fillStyle = this.data[i].color; ctx.fillRect(x, y - 5, 12, 12); ctx.fillStyle = '#333333'; ctx.fillText(`${this.data[i].label} (${this.data[i].value})`, x + 18, y); } } private switchMode(ring: boolean): void { this.isRing = ring; this.drawChart(); } build() { Column() { Text('数据可视化') .fontSize(26) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 8 }) Text('饼图 / 环形图') .fontSize(15) .fontColor('#888') .margin({ bottom: 10 }) // 切换按钮 Row() { Button('饼图') .type(ButtonType.Capsule) .backgroundColor(!this.isRing ? '#1976D2' : '#EEEEEE') .fontColor(!this.isRing ? Color.White : '#333') .fontSize(16) .layoutWeight(1) .onClick(() => { this.switchMode(false); }) Button('环形图') .type(ButtonType.Capsule) .backgroundColor(this.isRing ? '#1976D2' : '#EEEEEE') .fontColor(this.isRing ? Color.White : '#333') .fontSize(16) .layoutWeight(1) .margin({ left: 10 }) .onClick(() => { this.switchMode(true); }) } .width('80%') .margin({ bottom: 12 }) Canvas() .width('100%') .height(420) .backgroundColor('#FFFFFF') .onReady((event) => { let ctx = event.context as CanvasRenderingContext2D; this.onCanvasReady(ctx); }) Text('💡 使用 Canvas arc 方法绘制扇形,通过内外半径实现环形效果') .fontSize(12) .fontColor('#AAA') .width('90%') .textAlign(TextAlign.Center) .margin({ top: 8 }) } .width('100%') .height('100%') .backgroundColor('#FAFAFA') } }

代码逻辑很清晰:drawChart函数承担所有绘制工作,根据isRing决定画实心扇形还是环形扇区。数据数组data包含标签、数值和颜色。每次切换模式或数据变化时重绘。标签绘制会跳过占比小于 5% 的扇区,避免文字挤在一起。图例用简单的行列布局,在 Canvas 底部绘制。

运行效果

把代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上方显示标题,中间一张彩色饼图,五色扇区比例分明,每个大扇区中心标着百分比,像 “34%”“21%”。下方图例列出流量来源和数值。点一下“环形图”按钮,饼图中间立刻挖出一个空心圆,扇区变成厚环,整体立刻显得精致了几分。再点“饼图”又恢复实心。Canvas 绘制流畅,切换瞬间完成,没有任何延迟。

总结

这个小工具把数据可视化的几个核心技能串在了一起:

  • 饼图/环形图的绘制arc方法的灵活应用,环形扇区通过双弧线构建,是 Canvas 中经典的图形绘制手段。
  • 角度与百分比的换算:将数据占比转换为弧度扫过的角度,再配合起始角度累加,实现扇区连续排列。
  • 标签和布局设计:在扇形中心放置文字需要计算中点角度和半径,图例则用简单的行列布局逻辑。
  • 数据驱动绘图:通过@State绑定的数据数组和模式标记,界面变化自动触发 Canvas 重绘,整个过程无手动 DOM 操作。

如果后续想扩展,可以给数据加点交互,比如点击扇区高亮、悬浮显示详细数值,或者加一个简单的动画让扇区从零度展开。Canvas 在手,数据可视化就不再是第三方库的专利,想怎么画就怎么画。

http://www.jsqmd.com/news/952104/

相关文章:

  • 提升stm32f103c8t6开发效率:用快马一键生成uart、adc、定时器驱动模块
  • 深圳办公 ai 培训机构哪家值得合作:权威深度 TOP5 推 - 13724980961
  • java键盘录入
  • 深圳办公 ai 培训机构哪家口碑好:权威榜单专业测评攻略 - 19120507004
  • DMXAPI:企业大模型 API 集中采购服务商,优化企业 AI 采购全链路成本
  • 2026年当前,临沂企业如何选择专业财税咨询品牌?深度解析山东儒蒙企业服务有限公司 - 2026年企业资讯
  • 深圳办公 ai 培训机构哪家值得选择:独家榜单专业必读攻略 - 17329971652
  • FPG平台:风险提示的逻辑盘点
  • 超过100家荷兰酒店遭遇数据泄露,导致客人预订数据泄露
  • 96% 游戏公司都用 AI,为什么成功落地的只有 10%??成使用了AI工具。但在GDC 2026的行业状态调查,以及腾讯云发布的游戏行业白皮书中显示,仅有10%-15%的公司建立起支持大规模 AI
  • 线性dp-LIS题目1
  • 广州搬家公司哪家保护措施做的好:最新 TOP5 深度推荐 - 13425704091
  • 深入 Kubernetes Service 底层:解析 IPVS 流量转发与零中断平滑升级
  • 底部工具栏
  • TQVaultAE终极指南:三步掌握泰坦之旅无限仓库管理神器
  • 当数据消失时:TestDisk与PhotoRec如何成为你的数字救生员
  • 3000-4000元实况拍照手机横评:4款热门手机谁更值得买?
  • 3步搞定安卓应用安装:APK Installer让你的Windows电脑变身移动应用中心
  • 深圳办公 ai 培训机构哪家性价比高:独家 TOP5 深度解 - 13724980961
  • 2026 年 GEO 优化公司推荐名单:6 月国内 TOP10 服务商综合测评 + GEO 概念详解 - 玖叁鹿
  • CSAPP=系统硬件组成 + CPU 如何执行程序
  • [智能体-264]:Embedding 通俗发展史(四段式,大白话,从远古→现在 RAG)
  • Hello Agent 学习第一天
  • 深圳办公 ai 培训机构哪家值得信赖:五大机构最新专业测评 - 17329971652
  • 别再死记ResNet了!用PyTorch从零实现DenseNet-121,搞懂‘密集连接’到底好在哪
  • 被37所重点中小学内部传阅的《AI教学整合避坑手册》(含18个真实失败案例+可审计整改清单)
  • 2026乐清疏通马桶、下水道哪家好?4家优质商家测评信息,优选道道通! - 极速版本
  • 大优势揭秘,香港业主全屋定制为什么都选深圳RERA源木匠心 - 产品测评官
  • 利用人工智能破解中世纪密码
  • ai赋能jenkins:用快马平台智能生成与优化持续集成流水线脚本