OpenHarmony-JS封装canvas之折线图
https://harmonyos.51cto.com
前言
我们已经在之前实现了简易的柱状图,那么同理,我们该如何运用鸿蒙Jacascript实现简易的折线图呢?
折线图
我们在讲柱状图时,已经详细的描述了如何绘制我们的坐标轴,折线图的坐标轴与柱状图坐标轴的绘制方法几乎相同,所以在本文我们将不会讨论。关于坐标轴绘制的讲解与箭头绘制的讲解,大家可以查看上一期的柱状图绘制:
我们非常希望读者可以完全读懂柱状图的这一期文章,因为在折线图的绘制中使用到的坐标点,都在该篇文章中详细的解释过。
坐标轴的绘制
与柱状图类似,我们首先定义出绘制折线图时,我们将用到的数据:
obj:{
chartZone: [], //图表左上角与右下角坐标数组
yAxisLable: [], //Y轴内容
yMax: 0, //Y轴最大值
guideLine:true, //是否存在辅助线
dataPoint: true, //是否存在数据点
xAxisLable: [], //x轴内容
data: [], //数据内容
lineStyle:{
line_width: 0, //数据条的宽
color: '#1abc9c', //折线的颜色
radius: 0 //数据点半径
},
axisArrow:{
size:'2', //箭头因子大小
color:'red' //箭头填充色
},
dataStyle:{
radius: 5,
color:"#ee6587"
}
我们绘制折线图的总函数也与柱状图的绘制函数相同:
draw() {
this.drawAxis()
this.drawYLables()
this.drawXLables()
this.drawData()
this.drawArrow()
this.drawArrowY()
},
再次给出绘制坐标轴与坐标轴上内容的代码:
//绘制X,Y坐标轴
drawAxis() {
let chartZone = this.option.chartZone
const el = this.$element('the-canvas');
const context = el.getContext('2d');
context.save()
context.lineWidth = 2
context.strokeStyle = '#353535'
context.beginPath()
context.moveTo(chartZone[0], chartZone[1])
context.lineTo(chartZone[0], chartZone[3])
context.lineTo(chartZone[2], chartZone[3])
context.stroke()
},
//绘制y轴坐标
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.strokeStyle = '#353535';
context.beginPath();
context.moveTo(option.chartZone[0] - 10, option.chartZone[3] - index * gap);
context.lineTo(option.chartZone[0], option.chartZone[3] - index * gap)
context.stroke()
})
},
//绘制x轴坐标
drawXLables() {
const el = this.$element('the-canvas');
const context = el.getContext('2d');
let labels = this.option.xAxisLable;
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 / 2, option.chartZone[3] + 30);
//绘制小间隔
context.beginPath();
context.strokeStyle = '#353535';
context.moveTo(option.chartZone[0] + (index + 1) * gap, option.chartZone[3]);
context.lineTo(option.chartZone[0] + (index + 1) * gap, option.chartZone[3] + 10)
context.stroke()
//存储偏移量
option.offsetXLabel = offset / 2
})
},
数据的绘制
接下来我们进入重点:绘制数据。绘制数据将由三个模块组成:绘制折线,绘制虚线辅助线,绘制数据点。
其实,为了便于理解,我们应该先弄明白数据点的坐标,再思考如何绘制折线与虚线,但是在canvas中,先绘制的内容会被压在最底层,而后绘制的内容则会出现在最上层,所以我们绘制的顺序应该为:折线,虚线,数据点。
绘制折线
折线图的绘制实际上要比柱状图简单:柱状图的难点在于x轴上的小间隔要居中于数据条的底部,我们需要因此对数据条的坐标做处理,然而在折线图中则不需要做这种类似的处理,在折线图中,数据点的Y坐标即为数据的高度坐标,数据点的X坐标即为X坐标轴上小间隔的X坐标。所以折现的实现,就是在遍历坐标点的同时,将线lineTo到这个数据点。
绘制虚线
绘制虚线的思路则为:在遍历数据点的同时,从Y轴上数据点的高度所在位置lineTo数据点所在的位置,再lineTo数据点所在X轴上对应的小间隔所在的位置。
绘制数据点
本篇文章将数据点的样式设置为普通的圆点,遍历数据点,将数据点的坐标赋值给arc,并自定义圆点的半径大小与颜色。
数据绘制的代码如下所示:
//绘制数据
drawData() {
const el = this.$element('the-canvas');
const context = el.getContext('2d');
let data = this.option.data; //数据点坐标
let xLength = (this.option.chartZone[2] - this.option.chartZone[0]) * 0.96; //线段尾部留白后x轴长
let gap = xLength / this.option.xAxisLable.length; //x轴间隙
//缓存从数据值到坐标距离的比例因子
let yFactor = (this.option.chartZone[3] - this.option.chartZone[1]) * 0.98 / this.option.yMax
let activeX = 0; //记录绘制过程中当前点的坐标
let activeY = 0; //记录绘制过程中当前点的y坐标
context.strokeStyle = this.option.lineStyle.color || '#1abc9c'; //设定折线的颜色
context.lineWidth = this.option.lineStyle.line_width; //设定折线的线宽
//绘制折线
context.beginPath();
context.moveTo(this.option.chartZone[0], this.option.chartZone[3]); //先将起点移动至0,0坐标
for (let i = 0; i < data.length; i++) {
activeX = this.option.chartZone[0] + (i + 1) * gap;
activeY = this.option.chartZone[3] - data[i] * yFactor;
context.lineTo(activeX, activeY);
context.stroke();
}
context.restore()
//绘制数据点辅助虚线
context.strokeStyle = '#a29d9d'
context.setLineDash([10, 20])
context.beginPath()
if (this.option.guideLine == true) {
for (let i = 0; i < data.length; i++) {
context.moveTo(this.option.chartZone[0], this.option.chartZone[3] - data[i] * yFactor)
activeX = this.option.chartZone[0] + (i + 1) * gap;
activeY = this.option.chartZone[3] - data[i] * yFactor;
context.lineTo(activeX, activeY)
context.lineTo(activeX, this.option.chartZone[3])
context.lineTo(activeX, this.option.chartZone[3])
context.stroke()
}
}
//绘制数据
if (this.option.dataPoint == true) {
for (let i = 0; i < data.length; i++) {
activeX = this.option.chartZone[0] + (i + 1) * gap;
activeY = this.option.chartZone[3] - data[i] * yFactor;
context.fillStyle = this.option.dataStyle.color
context.beginPath()
context.arc(activeX, activeY, this.option.dataStyle.radius, 0, 2 * Math.PI, false)
context.fill()
context.closePath()
}
}
context.restore()
},
箭头的绘制
我们照例给出箭头函数的代码。
drawArrow() {
const el = this.$element('the-canvas');
const context = el.getContext('2d');
let factor = this.option.axisArrow.size;
context.save()
// context.translate(this.option.chartZone[2], this.option.chartZone[3])
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() {
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()
},
总结
以上就实现了折线图的基本功能,我们完全可以直接将绘制折线的函数放到柱状图的组件中实现折线图与柱状图共存的图表,以满足特定的需求。我们应该要思考的点在于:如何实现折线图的hover效果?如何使用贝塞尔曲线来实现平滑的折线呢?