HarmonyOS 6.0 数据可视化图表

📅 2026/7/21 12:02:55
HarmonyOS 6.0 数据可视化图表
图表是数据展示最直观的方式。HarmonyOS没有内置的图表库没有echarts那种图表要么用Canvas自己画要么用Progress/DataPanel这类简化组件。这篇把柱状图、折线图、饼图、环形图全部用Canvas实现一遍顺便讲清楚绘图流程。Canvas基础Canvas绘图需要RenderingContext2Dprivatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);build(){Canvas(this.context).width(100%).height(200).onReady((){this.drawChart();})}onReady是关键——Canvas组件准备完毕后才能绑定context绘图。在aboutToAppear里画是画不出来的因为Canvas还没挂载。RenderingContextSettings(true)表示开启抗锯齿。坐标系Canvas坐标系左上角是(0,0)x向右增长y向下增长。画图表需要先translate到绘图区域起点letctx:CanvasRenderingContext2Dthis.context;letpadding:number30;letchartWidth:number320-padding*2;letchartHeight:number200-padding*2;padding给坐标轴和标签留空间。chartWidth/chartHeight是实际绘图区域。柱状图privatedrawBarChart():void{letctx:CanvasRenderingContext2Dthis.context;letpadding:number30;letw:number320;leth:number200;letchartW:numberw-padding*2;letchartH:numberh-padding*2;letdata:number[][65,45,80,55,90,70];letmaxVal:number100;letbarWidth:numberchartW/data.length*0.6;letgap:numberchartW/data.length;// 清空画布ctx.clearRect(0,0,w,h);// 画网格线ctx.strokeStyle#eeeeee;ctx.lineWidth1;for(leti:number0;i4;i){lety:numberpadding(chartH/4)*i;ctx.beginPath();ctx.moveTo(padding,y);ctx.lineTo(w-padding,y);ctx.stroke();}// 画柱子letcolors:string[][#1a73e8,#43a047,#ff9800,#e53935,#9c27b0,#00897b];for(leti:number0;idata.length;i){letx:numberpaddinggap*i(gap-barWidth)/2;letbarH:numberdata[i]/maxVal*chartH;lety:numberpaddingchartH-barH;ctx.fillStylecolors[i%colors.length];ctx.fillRect(x,y,barWidth,barH);// 数据标签ctx.fillStyle#333333;ctx.font10px sans-serif;ctx.textAligncenter;ctx.fillText(data[i].toString(),xbarWidth/2,y-5);}}clearRect清空画布是必要的第一步——否则重绘时旧内容不会消失。fillRect画柱子fillText画标签。每根柱子的x坐标根据gap和barWidth计算。折线图privatedrawLineChart():void{letctx:CanvasRenderingContext2Dthis.context;letpadding:number30;letw:number320;leth:number200;letchartW:numberw-padding*2;letchartH:numberh-padding*2;letdata:number[][65,45,80,55,90,70];letmaxVal:number100;ctx.clearRect(0,0,w,h);// 网格线ctx.strokeStyle#eeeeee;ctx.lineWidth1;for(leti:number0;i4;i){lety:numberpadding(chartH/4)*i;ctx.beginPath();ctx.moveTo(padding,y);ctx.lineTo(w-padding,y);ctx.stroke();}// 折线ctx.strokeStyle#1a73e8;ctx.lineWidth2;ctx.beginPath();for(leti:number0;idata.length;i){letx:numberpadding(chartW/(data.length-1))*i;lety:numberpaddingchartH-(data[i]/maxVal*chartH);if(i0){ctx.moveTo(x,y);}else{ctx.lineTo(x,y);}}ctx.stroke();// 数据点ctx.fillStyle#1a73e8;for(leti:number0;idata.length;i){letx:numberpadding(chartW/(data.length-1))*i;lety:numberpaddingchartH-(data[i]/maxVal*chartH);ctx.beginPath();ctx.arc(x,y,4,0,6.28);ctx.fill();}}moveTo画起点lineTo画后续点stroke连成线。arc画圆点6.28≈2π画完整圆。折线图的核心是坐标转换数据值→Canvas坐标。面积图面积图在折线图基础上加填充// 在折线stroke之后ctx.fillStylergba(26, 115, 232, 0.15);ctx.beginPath();ctx.moveTo(padding,paddingchartH);// 左下角for(leti:number0;idata.length;i){letx:numberpadding(chartW/(data.length-1))*i;lety:numberpaddingchartH-(data[i]/maxVal*chartH);ctx.lineTo(x,y);}ctx.lineTo(paddingchartW,paddingchartH);// 右下角ctx.closePath();ctx.fill();关键先从左下角moveTo然后lineTo所有数据点最后lineTo右下角closePath闭合路径fill填充半透明色。饼图饼图用arc画扇区每个扇区的起始角度根据数据占比计算privatedrawPieChart():void{letctx:CanvasRenderingContext2Dthis.context;letcenterX:number160;letcenterY:number120;letradius:number80;letvalues:number[][35,25,20,20];letcolors:string[][#1a73e8,#43a047,#ff9800,#e0e0e0];letlabels:string[][技术,设计,运营,其他];lettotal:number0;for(leti:number0;ivalues.length;i){totalvalues[i];}ctx.clearRect(0,0,320,240);letstartAngle:number-Math.PI/2;// 从12点方向开始for(leti:number0;ivalues.length;i){letsweepAngle:number(values[i]/total)*2*Math.PI;ctx.fillStylecolors[i];ctx.beginPath();ctx.moveTo(centerX,centerY);ctx.arc(centerX,centerY,radius,startAngle,startAnglesweepAngle);ctx.closePath();ctx.fill();// 标签letmidAngle:numberstartAnglesweepAngle/2;letlabelX:numbercenterXMath.cos(midAngle)*(radius20);letlabelY:numbercenterYMath.sin(midAngle)*(radius20);ctx.fillStyle#333333;ctx.font11px sans-serif;ctx.textAligncenter;ctx.fillText(labels[i] values[i].toString()%,labelX,labelY);startAnglesweepAngle;}}起始角度-PI/2是12点方向默认3点方向是0。sweepAngle根据数据占比计算。每个扇区moveTo圆心→arc画弧→closePath闭合→fill填充。标签位置在扇区中间角度、半径20的位置。cos/sin计算极坐标到直角坐标。环形图甜甜圈图环形图就是饼图中间挖个圆// 画完饼图后ctx.fillStyle#ffffff;ctx.beginPath();ctx.arc(centerX,centerY,radius*0.6,0,2*Math.PI);ctx.fill();// 中间文字ctx.fillStyle#333333;ctx.fontbold 18px sans-serif;ctx.textAligncenter;ctx.fillText(100%,centerX,centerY6);用背景色画一个更小的圆覆盖饼图中心。0.6倍半径形成甜甜圈效果。DataPanel组件DataPanel是内置的环形图组件比Canvas简单得多DataPanel({values:[30,20,15,35],max:100,type:DataPanelType.Circle}).width(160).height(160).colors([[0x1a73e8,0],[0x43a047,0.3],[0xff9800,0.5],[0xe53935,0.8]])DataPanelType有两个值Circle环形和Line线形。values是各段数值max是总值。colors数组中每个元素是[color, offset]。但DataPanel功能有限——不支持标签、动画、交互。要做复杂图表还是得用Canvas。图表动画图表数据变化时加动画效果StateanimProgress:number0;aboutToAppear():void{setTimeout((){this.animProgress1;},100);}// 在drawBarChart中letbarH:numberdata[i]/maxVal*chartH*this.animProgress;animProgress从0到1柱子高度乘以这个系数实现从0增长到目标高度的动画。用animation修饰器或animateTo驱动animProgress变化。更精细的控制用setIntervalprivatestartAnimation():void{this.animProgress0;letstartTime:numberDate.now();letduration:number800;lettimer:numbersetInterval((){letelapsed:numberDate.now()-startTime;if(elapsedduration){this.animProgress1;clearInterval(timer);}else{this.animProgresselapsed/duration;}this.context.clearRect(0,0,320,200);this.drawBarChart();},16);}每16ms一帧60fps计算进度重绘Canvas。setInterval在aboutToDisappear中clearInterval清理。点击交互Canvas本身不处理点击需要用onClick计算点击位置对应哪个数据Canvas(this.context).width(100%).height(200).onReady((){this.drawChart();}).onClick((event:ClickEvent){letx:numberevent.x;lety:numberevent.y;// 判断点击了哪个柱子/数据点this.handleChartClick(x,y);})event.x/y是点击位置跟绘图坐标对应。判断哪个柱子被点击计算x落在哪个gap区间。绘图性能Canvas每次重绘都要清空重画——没有局部更新能力。大数据量时注意避免频繁重绘数据不变就不调draw方法减少路径数量多条线合用一个beginPath离屏Canvas缓存OffscreenCanvas预渲染静态部分降低帧率动画场景30fps就够了不必60fps踩坑清单问题原因解决Canvas空白在onReady之前绘图移到onReady回调里重绘时残影没clearRect每次draw前clearRectarc画圆不闭合没closePathbeginPath→arc→closePath→fill文字对不齐textAlign默认start设textAlign‘center’坐标偏移没考虑padding所有坐标加上padding偏移饼图从3点开始起始角度0是3点方向用-PI/2从12点开始DataPanel颜色不对colors格式错[[color, offset], …]不是[color, offset]动画卡顿setInterval没清aboutToDisappear中clearIntervalonClick坐标不准Canvas有缩放用display.densityPixels换算fillText乱码font格式不对用’14px sans-serif’格式图表开发的核心是坐标转换——把数据值映射到Canvas像素坐标。搞清楚padding、chartWidth、chartHeight的计算任何图表都能画出来。