HarmonyOS7 用 Canvas 画柱状图并不难:CanvasBarChartBasics 全流程拆解

文章目录
前言

一提到数据可视化,很多人第一反应就是找图表库。这个思路没问题,但有些场景下,你未必需要一整套库。比如只是做一个轻量周报图、设备状态面板、运营数据卡片,自己用 Canvas 画一个简单柱状图,反而更灵活,依赖也更轻。
问题是,图表跟普通图形不一样。它不是“画几个柱子”就完了,还涉及坐标区域、留白、标签、刻度、数值位置。这也是为什么不少人一开始觉得图表很难。

这个案例正好给了一个很好的起点:手写一张最基础的柱状图。
这页案例的重点是什么
它不是要做一个专业图表库,而是把最基础的数据图表达清楚。里面已经包含了柱状图最关键的几块结构:
- 数据数组和标签数组
- 画布内边距
- Y 轴和底部基线
- 网格线与刻度文字
- 柱子本身
- 顶部数值和底部标签
你把这些关系弄明白,以后换成折线图、面积图、进度图,思路都能迁移。
完整代码
import { DEMO_BG_COLOR, DEMO_CARD_COLOR, DEMO_THEME_COLOR, DEMO_SUBTEXT_COLOR } from './types'
interface PaddingConfig {
top: number
bottom: number
left: number
right: number
}
@Entry
@Component
struct CanvasBarChartBasics {
@State isShow: boolean = true
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private data: number[] = [65, 80, 45, 90, 55, 75, 60]
private labels: 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(() => {
const w = 380
const h = 220
const padding: PaddingConfig = { top: 20, bottom: 40, left: 40, right: 20 }
const chartW = w - padding.left - padding.right
const chartH = h - padding.top - padding.bottom
const barW = chartW / this.data.length * 0.6
const gap = chartW / this.data.length * 0.4
this.context.clearRect(0, 0, w, h)
this.context.strokeStyle = '#E0E0E0'
this.context.lineWidth = 1
this.context.beginPath()
this.context.moveTo(padding.left, padding.top)
this.context.lineTo(padding.left, padding.top + chartH)
this.context.lineTo(w - padding.right, padding.top + chartH)
this.context.stroke()
for (let i = 0; i <= 4; i++) {
const y = padding.top + chartH * (1 - i / 4)
this.context.strokeStyle = '#F0F0F0'
this.context.beginPath()
this.context.moveTo(padding.left, y)
this.context.lineTo(w - padding.right, y)
this.context.stroke()
this.context.font = '10px sans-serif'
this.context.fillStyle = '#AAAAAA'
this.context.fillText('' + 25 * i, padding.left - 30, y + 3)
}
const colors = ['#4D96FF', '#6BCB77', '#FFD93D', '#FF6B6B', '#9B59B6', '#FFA500', '#4ECDC4']
for (let i = 0; i < this.data.length; i++) {
const x = padding.left + (chartW / this.data.length) * i + gap / 2
const barH = (this.data[i] / 100) * chartH
const y = padding.top + chartH - barH
this.context.fillStyle = colors[i]
this.context.fillRect(x, y, barW, barH)
this.context.font = '10px sans-serif'
this.context.fillStyle = '#666666'
this.context.textAlign = 'center'
this.context.fillText('' + this.data[i], x + barW / 2, y - 5)
this.context.fillText(this.labels[i], x + barW / 2, padding.top + chartH + 18)
}
})
.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 是图表的数据骨架
案例先定义了两组数组:
private data: number[] = [65, 80, 45, 90, 55, 75, 60]
private labels: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
这就是图表最基本的输入。
data 决定柱子的高度,labels 决定底部每根柱子对应的说明。很多人一开始就急着画,其实真正应该先想清楚的是:数据结构长什么样。
如果输入结构乱,后面坐标、标签、颜色都会跟着乱。
为什么一定要先留 padding
这句是全篇里最容易被低估的代码:
const padding: PaddingConfig = { top: 20, bottom: 40, left: 40, right: 20 }
图表不是填满整块画布就完事。你必须给坐标轴、刻度、标签、顶部数值留空间。
所以这里把画布总尺寸 w、h 再减去内边距,得到真正的图表可用区域:
const chartW = w - padding.left - padding.right
const chartH = h - padding.top - padding.bottom
这个思路特别重要。以后你做任何可视化,几乎都要先从“总空间”和“绘图区”拆开。
坐标轴和基线怎么画
案例用路径画了左侧轴线和底部基线:
this.context.beginPath()
this.context.moveTo(padding.left, padding.top)
this.context.lineTo(padding.left, padding.top + chartH)
this.context.lineTo(w - padding.right, padding.top + chartH)
this.context.stroke()
这里其实把图表的边界感一下子立起来了。即使你还没画柱子,用户也能感觉到“这是个数据区域”。
很多轻量图表不一定要完整包围框,但底线和一条纵向参考轴通常很有必要。
网格线为什么很关键
案例里用循环画了五条水平网格线:
for (let i = 0; i <= 4; i++) {
const y = padding.top + chartH * (1 - i / 4)
this.context.moveTo(padding.left, y)
this.context.lineTo(w - padding.right, y)
}
它的作用不是装饰,而是帮助读数。
没有网格线时,用户只能看出哪根柱子高、哪根柱子低;有了刻度线和左侧数值,他才更容易估出这是 50、75 还是接近 100。
数据图的本质不是“好看”,而是“能读”。
柱宽和间距怎么分配
案例把每一列的总空间拆成两部分:
const barW = chartW / this.data.length * 0.6
const gap = chartW / this.data.length * 0.4
这是一种非常实用的比例分配法。每组位置 60% 给柱子,40% 给空隙。这样柱子不会挤在一起,也不会显得太散。
这类写法的好处是,数据项数量变了,整体仍然能保持相对平衡。
柱子的高度怎么从数据映射出来
真正的数据映射就在这两句:
const barH = (this.data[i] / 100) * chartH
const y = padding.top + chartH - barH
第一句把数据值映射成柱高。
第二句决定柱子从哪里开始画。因为 Canvas 的 y 轴是向下增长的,所以柱子要想“从底部往上长”,就必须先算出顶部坐标。
这一步是很多人最容易算反的地方。
顶部数值和底部标签为什么值得保留
案例里每根柱子都画了两个文字:
this.context.fillText('' + this.data[i], x + barW / 2, y - 5)
this.context.fillText(this.labels[i], x + barW / 2, padding.top + chartH + 18)
这两个信息点其实很关键。
顶部数值让用户不必用眼睛估计;底部标签让用户知道这一列到底代表哪一天。它们一上,图表就从“可视块”变成了“可阅读的数据表达”。
颜色数组为什么也值得单独设计
案例给每根柱子配了不同颜色:
const colors = ['#4D96FF', '#6BCB77', '#FFD93D', '#FF6B6B', '#9B59B6', '#FFA500', '#4ECDC4']
这适合演示效果,但真实业务里要看场景。
如果强调每一天是独立分类,多色可以;如果强调同一维度的连续数据,统一色系往往更专业。比如一组销售额柱子通常不必七种颜色全上。
这个案例怎么改成业务图表
稍微改一下,这页就能变成很多业务组件。
比如:
- 把
labels换成设备编号,做设备监测面板 - 把
labels换成月份,做月度趋势卡片 - 把
data接成接口返回值,做真实报表模块 - 把颜色策略改成阈值色,做异常预警图
也就是说,这个案例已经不是玩具了,它其实是个可复用的最小图表骨架。
常见坑提醒
第一个坑,是没有先减掉 padding,结果标签和柱子全贴边。
第二个坑,是 textAlign 改成 center 后忘了恢复,不过这个案例后续没有再写别的内容,所以暂时没出问题。
第三个坑,是数据最大值写死为 100。如果真实数据不是这个量级,需要先做动态最大值计算。
第四个坑,是直接把太多标签塞到底部,文字会重叠。数据一多,布局策略要变。
写在最后
柱状图这件事,看起来像是图形问题,实际上更像空间和数据关系问题。只要你把绘图区、比例映射、标签位置和信息层级想清楚,用 Canvas 画一张清爽的图并不难。
这一页特别值得反复练,因为它已经把 Canvas 从“画图”推进到了“表达数据”。这一步走通,后面的应用场景会一下子宽很多。
更多推荐


所有评论(0)