# HistogramDemo **Repository Path**: dot_happydz_admin/HistogramDemo ## Basic Information - **Project Name**: HistogramDemo - **Description**: 鸿蒙JS柱状图 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-02-25 - **Last Updated**: 2024-02-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ArkUI(JS)画布组件canvas:自定义柱状图 ### 前言 最近项目中有柱状图的功能,看了下JS中的组件chart,发现并不适用要求,研究之后用canvas动手画一个。 ### 项目说明 本项目基于ArkUI中JS扩展的类Web开发范式,关于语法和概念直接看官网官方文档地址:[基于JS扩展的类Web开发范式1](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ui-js-overview-0000000000500376) [基于JS扩展的类Web开发范式2](https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-components-versions-0000001185761140) 工具版本:DevEco Studio 3.0 Beta2 SDK版本:3.0.0.1(API Version 7 Beta2) [画布组件canvas](https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-components-canvas-canvas-0000000000621808):提供画布组件。用于自定义绘制图形 ### 效果演示 默认选中第一条,点击柱状图,切换选中效果。 **但是发现绘制复杂一点的内容,调用清屏接口clearRect()后,重新绘制内容会闪烁,在官网论坛上面问了其他人,也都有遇到此问题,希望官方早点修复这个bug** ![](image/demo.gif) ### 用到的API | 方法/属性 | 解释 | | :---------------------- | :--------------------------------- | | getBoundingClientRect() | 获取组件大小和位置信息 | | getContext() | 获取canvas绘图上下文对象 | | save() | 对当前的绘图上下文进行保存 | | beginPath() | 创建一个新的绘制路径 | | moveTo() | 路径从当前点移动到指定点 | | lineTo() | 从当前点到指定点进行路径连接 | | stroke() | 进行边框绘制操作 | | arc() | 绘制弧线路径 | | fill() | 对封闭路径进行填充 | | restore() | 对保存的绘图上下文进行恢复 | | strokeStyle | 属性:设置描边的颜色 | | lineWidth | 属性:设置绘制线条的宽度 | | fillStyle | 属性:指定绘制的填充色 | | font | 属性:设置文本绘制中的字体样式 | | textAlign | 属性:设置文本绘制中的文本对齐方式 | | lineCap | 属性:指定线端点的样式 | ### 实现步骤 组件设置了上下左右间距,所以内容区域宽度 = 组件宽度 - 左间距 - 右间距,内容区域的高度 = 组件高度 - 上间距 - 下间距 1、画横线(x轴)、右边的文字 ![](image/demo1.png) ```javascript // 获取画布组件 const element = this.$refs.canvas; // 获取绘图上下文 const ctx = element.getContext('2d', {antialias: true}); // 获取组件大小和位置信息 const rect = element.getBoundingClientRect() // 测量y轴最大数的文本长度 const yTextMaxWidth = ctx.measureText('' + this.yAxisMaxValue).width // x轴起始坐标 const xAxisStart = this.paddingLeft // x轴结束坐标 = 组件宽度 - 右间距 - y轴文字最大宽度 const xAxisEnd = rect.width - this.paddingRight - yTextMaxWidth // y轴初始坐标 const yAxisStart = this.paddingTop // y轴结束坐标 = 组件高度 - 底间距 - 底部文字高度 - 文字顶间距 const yAxisEnd = rect.height - this.paddingBottom - 10 - this.xAxisTextTopPadding // y轴初始值-画文字 let yValue = this.yAxisMaxValue // y轴平均值-画文字 let yAverageValue = (this.yAxisMaxValue - this.yAxisMinValue) / this.yAxisDivide // y轴初始坐标 let yAxis = yAxisStart // 画横线、画右边文字 while (yAxis <= yAxisEnd) { // 画线 // 对当前的绘图上下文进行保存 ctx.save() // 线颜色 ctx.strokeStyle = this.xAxisColor // 创建一个新的绘制路径 ctx.beginPath() // 线条的宽度 ctx.lineWidth = this.xAxisWidth // 路径从当前点移动到指定点 ctx.moveTo(xAxisStart, yAxis) // 从当前点到指定点进行路径连接 ctx.lineTo(xAxisEnd, yAxis) // 进行边框绘制操作 ctx.stroke(); // 对保存的绘图上下文进行恢复 ctx.restore() // 画文本 ctx.save() // 指定绘制的填充色 ctx.fillStyle = this.yAxisTextColor // 设置文本绘制中的字体样式 ctx.font = this.yAxisTextFont // 设置文本绘制中的文本对齐方式:文本右对齐 ctx.textAlign = 'right' // 绘制填充类文本 ctx.fillText('' + yValue, xAxisEnd + yTextMaxWidth, yAxis + 2.5) ctx.restore() // 右边文本 yValue -= yAverageValue // 更新y轴坐标:每次加上y轴等分 yAxis += (yAxisEnd - yAxisStart) / this.yAxisDivide } ``` 2、画圆柱图、和底部文字 ![](image/demo2.png) ```javascript // 画x轴的实际宽度,两边柱状图不靠边 const xDrawWidth = xAxisEnd - xAxisStart - this.xAxisPadding * 2 // 起始点 let xAxis = this.paddingLeft + this.xAxisPadding // x轴平均值 let xAverageWidth = (xDrawWidth) / (this.chartData.length - 1) // 画x轴上的文字和数据 for (let i = 0; i < this.chartData.length; i++) { const item = this.chartData[i] // 画文本 ctx.save() ctx.fillStyle = this.xAxisTextColor ctx.font = this.xAxisTextFont ctx.textAlign = 'center' ctx.fillText(item.xData, xAxis, rect.height - this.paddingBottom) ctx.restore() // 画柱图 ctx.save() // 创建一个新的绘制路径 ctx.beginPath() // 线条的宽度,设置到最小,因为我们需要填充圆柱 ctx.lineWidth = 0.1 // 路径从当前点移动到指定点 ctx.moveTo(xAxis - this.columnarWidth / 2, yAxisEnd - this.xAxisWidth) // 根据数据比例得出 画的高度 const yDrawHeight = (yAxisEnd - yAxisStart) * item.yData / this.yAxisMaxValue // 从y轴结束开始画,值结束的坐标:yAxisValueEnd 为了显示圆角:+ 柱图宽度/2(圆的半径) const yAxisValueEnd = yAxisEnd - yDrawHeight + this.xAxisWidth + this.columnarWidth / 2 // 从当前点到指定点进行路径连接 ctx.lineTo(xAxis - this.columnarWidth / 2, yAxisValueEnd) // 画圆角 ctx.arc(xAxis, yAxisValueEnd, this.columnarWidth / 2, 3.14, 0) // 从当前点到指定点进行路径连接 ctx.lineTo(xAxis + this.columnarWidth / 2, yAxisEnd - this.xAxisWidth) // 进行边框绘制操作 ctx.stroke() // 填充颜色 ctx.fillStyle = this.columnarColor // 填充 ctx.fill() ctx.restore() // 更新x轴坐标:每次加上x轴等分 xAxis += xAverageWidth } ``` 3、点击选中效果:画柱图时,记录x轴的位置 ![](image/demo3.png) ```javascript // 存入索引和对应的坐标 this.columnarXArray = [] // 画x轴上的文字和数据 for (let i = 0; i < this.chartData.length; i++) { const item = this.chartData[i] const columnarX = { index: i, columnarX: xAxis } // 记录x轴,索引和对应的坐标 this.columnarXArray.push(columnarX) // 画文本 ...... // 文本颜色 ctx.fillStyle = this.selectIndex === i ? this.selectXAxisTextColor : this.xAxisTextColor ...... // 画选中的线 if(this.selectIndex === i){ // 画线 ctx.save() ctx.strokeStyle = this.yAxisColor ctx.lineCap = 'butt' ctx.lineWidth = this.yAxisWidth ctx.beginPath() ctx.moveTo(xAxis, yAxisEnd - this.xAxisWidth) ctx.lineTo(xAxis, this.paddingTop) ctx.stroke(); ctx.restore() } // 画柱图 ...... // 填充颜色 ctx.fillStyle = this.selectIndex === i ? this.selectColumnarColor : this.columnarColor ...... } ``` 根据触摸事件的坐标,判断是否在范围内,更新选中的索引 ```javascript // 触摸按下 onTouchStart(event) { console.log(event.touches[0].localX) // 点击x坐标,相对于组件 const clickX = event.touches[0].localX let lastSelectIndex = this.selectIndex // 筛选出点击的柱图索引 for (let i = 0; i < this.columnarXArray.length; i++) { let item = this.columnarXArray[i] if (Math.abs(item.columnarX - clickX) <= this.columnarWidth + 5) { this.selectIndex = item.index break } } // 重新绘制 if (this.selectIndex !== lastSelectIndex) { this.draw() } } ``` ### 项目地址 完整代码:[https://gitee.com/liangdidi/HistogramDemo](https://gitee.com/liangdidi/HistogramDemo) ### 总结 此项目并没有特别复杂的地方,注释也很详细,主要是xy轴的起始、结束位置的计算,屏幕的原点坐标(0,0)是在左上角,最后根据系统提供的api画出想要的效果。有些效果看起来很复杂,但是一步一步的拆解,懂得其原理之后,多练多用,也能做出炫酷的效果。 每天进步一点点、需要付出努力亿点点。