公司动态
HarmonyOS7 用 Canvas 画柱状图并不难:CanvasBarChartBasics 全流程拆解
文章目录前言这页案例的重点是什么完整代码data 和 labels 是图表的数据骨架为什么一定要先留 padding坐标轴和基线怎么画网格线为什么很关键柱宽和间距怎么分配柱子的高度怎么从数据映射出来顶部数值和底部标签为什么值得保留颜色数组为什么也值得单独设计这个案例怎么改成业务图表常见坑提醒写在最后前言一提到数据可视化很多人第一反应就是找图表库。这个思路没问题但有些场景下你未必需要一整套库。比如只是做一个轻量周报图、设备状态面板、运营数据卡片自己用Canvas画一个简单柱状图反而更灵活依赖也更轻。问题是图表跟普通图形不一样。它不是“画几个柱子”就完了还涉及坐标区域、留白、标签、刻度、数值位置。这也是为什么不少人一开始觉得图表很难。这个案例正好给了一个很好的起点手写一张最基础的柱状图。这页案例的重点是什么它不是要做一个专业图表库而是把最基础的数据图表达清楚。里面已经包含了柱状图最关键的几块结构数据数组和标签数组画布内边距Y 轴和底部基线网格线与刻度文字柱子本身顶部数值和底部标签你把这些关系弄明白以后换成折线图、面积图、进度图思路都能迁移。完整代码import{DEMO_BG_COLOR,DEMO_CARD_COLOR,DEMO_THEME_COLOR,DEMO_SUBTEXT_COLOR}from./typesinterfacePaddingConfig{top:numberbottom:numberleft:numberright:number}EntryComponentstruct CanvasBarChartBasics{StateisShow:booleantrueprivatesettings:RenderingContextSettingsnewRenderingContextSettings(true)privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings)privatedata:number[][65,80,45,90,55,75,60]privatelabels:string[][周一,周二,周三,周四,周五,周六,周日]build(){Column(){if(this.isShow){Scroll(){Column({space:16}){Text(Canvas 柱状图).fontSize(16).fontColor(DEMO_THEME_COLOR).fontWeight(FontWeight.Bold)Column(){Text(每周数据统计).fontSize(13).fontColor(#4D96FF).margin({bottom:8})Column(){Canvas(this.context).onReady((){constw380consth220constpadding:PaddingConfig{top:20,bottom:40,left:40,right:20}constchartWw-padding.left-padding.rightconstchartHh-padding.top-padding.bottomconstbarWchartW/this.data.length*0.6constgapchartW/this.data.length*0.4this.context.clearRect(0,0,w,h)this.context.strokeStyle#E0E0E0this.context.lineWidth1this.context.beginPath()this.context.moveTo(padding.left,padding.top)this.context.lineTo(padding.left,padding.topchartH)this.context.lineTo(w-padding.right,padding.topchartH)this.context.stroke()for(leti0;i4;i){constypadding.topchartH*(1-i/4)this.context.strokeStyle#F0F0F0this.context.beginPath()this.context.moveTo(padding.left,y)this.context.lineTo(w-padding.right,y)this.context.stroke()this.context.font10px sans-serifthis.context.fillStyle#AAAAAAthis.context.fillText(25*i,padding.left-30,y3)}constcolors[#4D96FF,#6BCB77,#FFD93D,#FF6B6B,#9B59B6,#FFA500,#4ECDC4]for(leti0;ithis.data.length;i){constxpadding.left(chartW/this.data.length)*igap/2constbarH(this.data[i]/100)*chartHconstypadding.topchartH-barHthis.context.fillStylecolors[i]this.context.fillRect(x,y,barW,barH)this.context.font10px sans-serifthis.context.fillStyle#666666this.context.textAligncenterthis.context.fillText(this.data[i],xbarW/2,y-5)this.context.fillText(this.labels[i],xbarW/2,padding.topchartH18)}}).width(100%).height(240).backgroundColor(#FAFAFA).borderRadius(8)}}.backgroundColor(DEMO_CARD_COLOR).padding(12).borderRadius(12)Column({space:6}){Text(数据详情).fontSize(14).fontColor(DEMO_THEME_COLOR).fontWeight(FontWeight.Bold)Row(){ForEach(this.labels,(label:string,_:number){Text(label).fontSize(11).fontColor(#666666).layoutWeight(1).textAlign(TextAlign.Center)})}.width(100%)Row(){ForEach(this.data,(val:number,_:number){Text(val).fontSize(13).fontColor(#1A1A1A).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)})}.width(100%)}.width(100%).backgroundColor(#E8F4FD).padding(14).borderRadius(12)}.width(100%)}}Text(CanvasBarChartBasics - 绘制每周柱状图).fontSize(12).fontColor(DEMO_SUBTEXT_COLOR).margin({top:12})}.width(100%).height(100%).backgroundColor(DEMO_BG_COLOR).padding(16)}}data和labels是图表的数据骨架案例先定义了两组数组privatedata:number[][65,80,45,90,55,75,60]privatelabels:string[][周一,周二,周三,周四,周五,周六,周日]这就是图表最基本的输入。data决定柱子的高度labels决定底部每根柱子对应的说明。很多人一开始就急着画其实真正应该先想清楚的是数据结构长什么样。如果输入结构乱后面坐标、标签、颜色都会跟着乱。为什么一定要先留padding这句是全篇里最容易被低估的代码constpadding:PaddingConfig{top:20,bottom:40,left:40,right:20}图表不是填满整块画布就完事。你必须给坐标轴、刻度、标签、顶部数值留空间。所以这里把画布总尺寸w、h再减去内边距得到真正的图表可用区域constchartWw-padding.left-padding.rightconstchartHh-padding.top-padding.bottom这个思路特别重要。以后你做任何可视化几乎都要先从“总空间”和“绘图区”拆开。坐标轴和基线怎么画案例用路径画了左侧轴线和底部基线this.context.beginPath()this.context.moveTo(padding.left,padding.top)this.context.lineTo(padding.left,padding.topchartH)this.context.lineTo(w-padding.right,padding.topchartH)this.context.stroke()这里其实把图表的边界感一下子立起来了。即使你还没画柱子用户也能感觉到“这是个数据区域”。很多轻量图表不一定要完整包围框但底线和一条纵向参考轴通常很有必要。网格线为什么很关键案例里用循环画了五条水平网格线for(leti0;i4;i){constypadding.topchartH*(1-i/4)this.context.moveTo(padding.left,y)this.context.lineTo(w-padding.right,y)}它的作用不是装饰而是帮助读数。没有网格线时用户只能看出哪根柱子高、哪根柱子低有了刻度线和左侧数值他才更容易估出这是 50、75 还是接近 100。数据图的本质不是“好看”而是“能读”。柱宽和间距怎么分配案例把每一列的总空间拆成两部分constbarWchartW/this.data.length*0.6constgapchartW/this.data.length*0.4这是一种非常实用的比例分配法。每组位置 60% 给柱子40% 给空隙。这样柱子不会挤在一起也不会显得太散。这类写法的好处是数据项数量变了整体仍然能保持相对平衡。柱子的高度怎么从数据映射出来真正的数据映射就在这两句constbarH(this.data[i]/100)*chartHconstypadding.topchartH-barH第一句把数据值映射成柱高。第二句决定柱子从哪里开始画。因为Canvas的y轴是向下增长的所以柱子要想“从底部往上长”就必须先算出顶部坐标。这一步是很多人最容易算反的地方。顶部数值和底部标签为什么值得保留案例里每根柱子都画了两个文字this.context.fillText(this.data[i],xbarW/2,y-5)this.context.fillText(this.labels[i],xbarW/2,padding.topchartH18)这两个信息点其实很关键。顶部数值让用户不必用眼睛估计底部标签让用户知道这一列到底代表哪一天。它们一上图表就从“可视块”变成了“可阅读的数据表达”。颜色数组为什么也值得单独设计案例给每根柱子配了不同颜色constcolors[#4D96FF,#6BCB77,#FFD93D,#FF6B6B,#9B59B6,#FFA500,#4ECDC4]这适合演示效果但真实业务里要看场景。如果强调每一天是独立分类多色可以如果强调同一维度的连续数据统一色系往往更专业。比如一组销售额柱子通常不必七种颜色全上。这个案例怎么改成业务图表稍微改一下这页就能变成很多业务组件。比如把labels换成设备编号做设备监测面板把labels换成月份做月度趋势卡片把data接成接口返回值做真实报表模块把颜色策略改成阈值色做异常预警图也就是说这个案例已经不是玩具了它其实是个可复用的最小图表骨架。常见坑提醒第一个坑是没有先减掉padding结果标签和柱子全贴边。第二个坑是textAlign改成center后忘了恢复不过这个案例后续没有再写别的内容所以暂时没出问题。第三个坑是数据最大值写死为 100。如果真实数据不是这个量级需要先做动态最大值计算。第四个坑是直接把太多标签塞到底部文字会重叠。数据一多布局策略要变。写在最后柱状图这件事看起来像是图形问题实际上更像空间和数据关系问题。只要你把绘图区、比例映射、标签位置和信息层级想清楚用Canvas画一张清爽的图并不难。这一页特别值得反复练因为它已经把Canvas从“画图”推进到了“表达数据”。这一步走通后面的应用场景会一下子宽很多。