春节不停更,此文正在参加「星光计划-春节更帖活动」 作者:焦以焜

前言

鸿蒙已经提供了chart组件来实现数据可视化的需求,那么,我们该如何自定义一个chart组件来实现数据可视化呢?本文将运用canvas来自定义一个简单的柱状图组件。

柱状图

在一切开始之前,我们首先需要创建一个画布,一切对canvas的操作,都将在这个画布上进行,所以我们需要把绘制的函数绑定在该canvas上。

<div >
    <canvas @touchstart='draw' id="the-canvas"
            style="width: {{canvasX}};height: {{canvasY}}; background-color: whitesmoke;;"></canvas>
</div>

我们知道一个柱状图由坐标轴,坐标轴内容,数据内容和箭头组成,显然我们绘制柱状图的函数也应该由绘制这几个部分的函数来组建而成。

draw() {
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        this.drawAxis()
        this.drawYLables()
        this.drawXLables()
        this.drawData()
        this.drawData2()
        this.drawArrowX()
        this.drawArrowY()
    },

我们同时也可以定义出,绘制柱状图所需要的一些数据。

option:{
        chartZone: [],   //图表左上角与右下角坐标数组
        yAxisLable: [],  //Y轴内容
        yMax: '', //Y轴最大值
        guideLine:'', //是否存在辅助线
        xAxisLable: [], //x轴内容
        data: [], //数据内容
        barStyle:{
            width: '',  //数据条的宽
            color: ''   //数据条的颜色
        },
        axisArrow:{
            size:'',    //箭头因子大小
            color:''    //箭头填充色
        }
}

绘制坐标轴

绘制X,Y轴

我们需要定义一个画图的区域chartZone来决定我们要在画布的哪个位置开始画我们的内容,chartZone的四个值为图表的左上角和右下角的坐标。注意,在canvas中,坐标的默认原点位置将不再是我们常规理解的位于坐标左下角的坐标的原点,而是处于canvas画布的左上方。

//绘制X,Y坐标轴
    drawAxis() {
        let chartZone = this.option.chartZone
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        context.lineWidth = 2
        context.strokeStyle = '#353535'
        context.moveTo(chartZone[0], chartZone[1])
        context.lineTo(chartZone[0], chartZone[3])
        context.lineTo(chartZone[2], chartZone[3])
        context.stroke()
    },

img_axis.png

绘制Y轴上的内容

让我们先来绘制Y轴上的内容。

我们用context.measureText(label).width来在画出字体之前就计算出字体的长度,再加一段距离用offset存储该值,留作用来计算画Y轴上数值内容的x坐标。

画Y轴时,我们一般不会直接用Y轴的真正长度来均分我们的Y轴数据,我们需要留一小节顶端来保持美感且在后续拿来画箭头。所以我们真正使用Y轴的长度为整体长度乘以0.98。(我们当然可以自定义这个数值)

代码如下:

drawYLables() {
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        context.save()
        let labels = this.option.yAxisLable;
        let yLength = (this.option.chartZone[3] - this.option.chartZone[1]) * 0.98
        let gap = yLength / (labels.length - 1)
        let option = this.option
        labels.forEach(function (label, index) {
            //绘制坐标文字
            //offset为y轴上内容起点与y轴的距离
            let offset = context.measureText(label).width + 20
            context.strokeStyle = '#eaeaea';
            context.font = "16px"
            context.fillText(label, option.chartZone[0] - offset, option.chartZone[3] - index * gap);
            context.restore()
            //绘制小间隔(坐标轴原点不需要绘制小间隔)
            if (index == 0) {
                return
            } else {
                context.beginPath();
                context.strokeStyle = '#353535';
                //我们在此处将小间隔的长度设置为10px
                context.moveTo(option.chartZone[0] - 10, option.chartZone[3] - index * gap);
                context.lineTo(option.chartZone[0], option.chartZone[3] - index * gap)
                context.stroke()
            }
            //绘制辅助线
            if (option.guideLine == true) {
                //x轴坐标不需要绘制辅助线
                if (index == 0) {
                    return
                } else {
                    context.beginPath()
                    context.strokeStyle = 'grey'
                    context.lineWidth = 2
                    context.moveTo(option.chartZone[0], option.chartZone[3] - index * gap)
                    context.lineTo(option.chartZone[2], option.chartZone[3] - index * gap)
                    context.stroke()
                }
            }
        })
    },

img_yaxis.png

将该截图结合代码一起看的话可以更方便理解。

绘制X轴上的内容

这部分与Y轴其实非常相似,且不需要做辅助线,需要注意,我们为了让X轴上的内容与小间隔的点对齐时,也使用到了context.measureText(label).width,将该值除以二后放入关于小间隔的横坐标运算,使得小间隔的点居中于文字。

我们需要记住我们这时候存储的偏移量offsetXLabel,在绘制数据的时候将会用到。

drawXLables() {
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        let labels = this.option.xAxisLable;
        //和Y轴真正使用到的长度类似,X轴我们设定只用了96%
        let xLength = (this.option.chartZone[2] - this.option.chartZone[0]) * 0.96
        let gap = xLength / (labels.length)
        let option = this.option
        labels.forEach(function (label, index) {
            //绘制坐标文字
            let offset = context.measureText(label).width
            context.save()
            context.strokeStyle = '#eaeaea';
            context.font = "18px"
            context.fillText(label, option.chartZone[0] + (index + 1) * gap - offset, option.chartZone[3] + 20);
            //绘制小间隔
            context.beginPath();
            context.strokeStyle = '#353535';
            context.moveTo(option.chartZone[0] + (index + 1) * gap - offset / 2, option.chartZone[3]);
            context.lineTo(option.chartZone[0] + (index + 1) * gap - offset / 2, option.chartZone[3] + 5)
            context.stroke()
            //存储偏移量
            option.offsetXLabel = offset / 2
        })

    },

img_xaxis.png

将该截图结合代码一起看的话可以更方便理解。

数据的绘制

画出数据条的难点应该在于高度和矩形的位置,我们应该如何计算出这两点呢?

我们可以通过比例来算出第i个数据条的高度,我们设需要的数据条高度为height,H为y轴的实际使用的高度,yMax为我们设定的y轴最大值,则: $$ \dfrac{height}{H} =\dfrac{data[i]}{yMax} $$ 显然:

$$ height =\dfrac{data[i]*H}{yMax} $$ img_data.png

所以我们画数据条所需要的y0坐标值等于chartZone[3] - height

那我们又该如何算出我们画数据条所需要的x0呢?还记得我们是如何作出x轴小间隔的吗?x0显然是小间隔的x坐标减去我们希望数据条宽度的一半,计算时请记得使用上我们刚才保存的offsetXLabel,计算方法如代码所示。

//柱状图
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        context.save()
        let data = this.option.data
        let xLength = (this.option.chartZone[2] - this.option.chartZone[0]) * 0.96
        let ylength = (this.option.chartZone[3] - this.option.chartZone[1]) * 0.98
        let gap = xLength / this.option.xAxisLable.length;
        let option = this.option
        data.forEach(function (item, index) {
            context.fillStyle = option.barStyle.color || '#1abc9c';
            let x0 = option.chartZone[0] + (index + 1) * gap - option.barStyle.width / 2 - option.offsetXLabel;
            let height = item / option.yMax * ylength
            let y0 = option.chartZone[3] - height
            let width = option.barStyle.width
            context.fillRect(x0, y0, width, height)
        })
        context.restore()

箭头的绘制

箭头的绘制就简单了许多,我们可以设定以个箭头大小因子,在箭头形状不变的情况下,我们可以直接通过改变箭头因子的大小来改变箭头的大小。

drawArrowX() {
        //绘制x轴箭头
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        let factor = this.option.axisArrow.size;
        context.save()
        context.beginPath()
        context.moveTo(this.option.chartZone[2], this.option.chartZone[3])
        context.lineTo(this.option.chartZone[2] + 2 * factor, this.option.chartZone[3] - 3 * factor)
        context.lineTo(this.option.chartZone[2] + 10 * factor, this.option.chartZone[3])
        context.lineTo(this.option.chartZone[2] + 2 * factor, this.option.chartZone[3] + 3 * factor)
        context.lineTo(this.option.chartZone[2], this.option.chartZone[3])
        context.globalAlpha = 0.7
        context.fillStyle = this.option.axisArrow.color
        context.fill()
        context.restore()
    },
    drawArrowY() {
        //绘制y轴箭头
        const el = this.$element('the-canvas');
        const context = el.getContext('2d');
        let factor = this.option.axisArrow.size;
        context.save()
        context.beginPath()
        context.moveTo(this.option.chartZone[0], this.option.chartZone[1])
        context.lineTo(this.option.chartZone[0] - 3 * factor, this.option.chartZone[1] - 2 * factor)
        context.lineTo(this.option.chartZone[0], this.option.chartZone[1] - 10 * factor)
        context.lineTo(this.option.chartZone[0] + 3 * factor, this.option.chartZone[1] - 2 * factor)
        context.lineTo(this.option.chartZone[0], this.option.chartZone[1])
        context.globalAlpha = 0.7
        context.fillStyle = this.option.axisArrow.color
        context.fill()
        context.restore()
    }

组件的使用

首先引用我们的组件,例如:

<drawbarchart canvas-x="{{X}}" canvas-y="{{Y}}" option="{{obj}}"></drawbarchart>

调用时,输入我们需要的数据内容,例如:

X:2560,
Y:1600,
obj:{
                chartZone: [50, 50, 1000, 600],   //图表左上角与右下角坐标数组
                yAxisLable: [0,100,200,300,400,500],  //Y轴内容
                yMax: '500', //Y轴最大值
                guideLine:true, //是否存在辅助线
                xAxisLable: ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"], //x轴内容
                data: [150,200,250,300,100,500,200], //数据内容
                barStyle:{
                    width: '30',  //数据条的宽
                    color: 'rgb(25, 183, 207)'   //数据条的颜色
                },
                axisArrow:{
                    size:'2',    //箭头因子大小
                    color:'red'    //箭头填充色
                }
        }

img_final.png

总结

本文仅仅实现了柱状图的基本功能,还有很多拓展的功能如:折线图与柱状图共存,多数据条的柱状图,数据条颜色的渐变,数据条的hover等等,笔者将在未来分享,笔者也将继续分享折线图、散点图等图表的实现,欢迎各位鸿蒙开发者一起讨论与研究,共勉。

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

::: hljs-center

21_9.jpg

:::