canvas绘制折线图
其大致思路如上一篇文章一样。需要的步骤主要也是分为
- 坐标轴绘制
- 绘制坐标轴
- 绘制刻度
- 折线绘制
在上一篇的文章基础上,为了更加优雅的代码复用,所以采用对象的方式来管理相关配置和方法。
基础配置
/**
* 初始化图形
* @param {string} id canvas的id
*/
function initChart(id) {
const canvas = document.getElementById(id)
const context = canvas.getContext('2d')
// 用一个颜色做底色
context.fillStyle = '#fafafa'
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = '#000000'
// 赋值属性
this.canvas = canvas
this.context = context
this.chartZone = [50, 50, 700, 450]
this.xAxisLable = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
this.yAxisLable = ['0', '100', '200', '300', '400']
this.yMax = 400
this.data = [60, 150, 240, 230, 390, 310, 80]
this._xLength = (this.chartZone[2] - this.chartZone[0]) * 0.98
this._yLength = (this.chartZone[3] - this.chartZone[1]) * 0.98
this._xLablePadding = 20
this.points = null
}
同上一篇文章一样,为了方便后续的工作,我们需要准备相应的配置项。但此次采用了对象的方式来管理。
坐标轴绘制
/**
* 绘制x轴
*/
initChart.prototype.drawXAxis = function() {
let gap = this._xLength / this.xAxisLable.length
let self = this
//绘制横线
this.context.moveTo(this.chartZone[0], this.chartZone[3])
this.context.lineTo(this.chartZone[2], this.chartZone[3])
this.context.strokeStyle = '#353535'
this.context.strokeWidth = 4
this.context.stroke()
//绘制刻度
this.xAxisLable.forEach(function (lable, index) {
self.context.font = '16px'
self.context.textAlign = 'center'
self.context.fillText(lable, self.chartZone[0] + (index + 0.5) * gap, self.chartZone[3] + self._xLablePadding, gap)
self.context.moveTo(self.chartZone[0] + (index + 0.5) * gap, self.chartZone[3] + 10)
self.context.lineTo(self.chartZone[0] + (index + 0.5) * gap, self.chartZone[3])
self.context.stroke()
})
return this
}
坐标轴的绘制和上一篇文章一样,没有什么大的变化。此处列出绘制x轴的源码,y轴类似。详细源码可见仓库中源码。
绘制折线图
/**
* 绘制折线图
*/
initChart.prototype.drawLine = function() {
let self = this
let gap = this._xLength / this.xAxisLable.length
this.data.forEach(function(item, index) {
let y = self.chartZone[3] - item * self._yLength / self.yMax
let x = self.chartZone[0] + (index + 0.5) * gap
if (index !== 0) {
// 如果不是第一个点,则从上一个点绘制到当前点
self.context.lineTo(x, y)
self.context.strokeStyle = '#1abc9c'
self.context.strokeWidth = 4
self.context.stroke()
}
// 绘制一个纵向的辅助线
self.context.beginPath()
self.context.moveTo(x, y)
self.context.lineTo(x, self.chartZone[3])
self.context.setLineDash([8, 8])
self.context.strokeStyle = '#aeaeae'
self.context.strokeWidth = 2
self.context.stroke()
self.context.setLineDash([])
// 绘制一个圆点来标记数据点
self.context.beginPath()
self.context.arc(x, y, 5, 0, 2 * Math.PI, false)
self.context.fillStyle = '#1abc9c'
self.context.fill()
// 开始新的路径绘制,将画笔移动到当前点,为绘制到下一个点做准备
self.context.beginPath()
self.context.moveTo(x, y)
})
return this
}
折线图的绘制中,为了绘制辅助的线和点,我们需要注意绘制顺序,在每次beginPath
的时候,相当于重新开始一条线的绘制。当然,也可以使用两次遍历数据源来绘制,逻辑结构也相对比较清晰。(在下面的示例中会见到这种做法)
为了让调用更加方便,我们可以加入一个方法统一来调用
/**
* 绘制折线图调用
*/
initChart.prototype.drawLineChart = function() {
this.drawXAxis().drawYAxis().drawLine()
}
曲线绘制
在上示例中,我们绘制出来的是一个折线图,如果我们想让线条更加流畅,绘制一条曲线图而非折线图,如下图所示,那么我们该如何做呢?
三次贝塞尔曲线
如想绘制出上图所示的一条曲线,我们则需要使用到CanvasRenderingContext2D.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
方法来绘制三次贝赛尔曲线。详细参数解析可见MDN文档。
三次贝赛尔曲线是由4个点来确定的,如何通过点来确定贝塞尔曲线的控制点位置,可参考文章《贝塞尔曲线控制点确定的方法》。
/**
* 将数据转化为坐标点
*/
initChart.prototype._getPoints = function() {
if (this.points !== null)
return this.points
let points = []
let gap = this._xLength / this.xAxisLable.length
let self = this
this.data.forEach(function(item, index) {
let y = self.chartZone[3] - self._yLength * item / self.yMax
let x = self.chartZone[0] + (index + 0.5) * gap
points.push({x,y})
})
this.points = points
return this.points
}
/**
* 获取3次贝塞尔曲线的点
*/
initChart.prototype._getBezierPoints = function() {
let _points = this._getPoints().slice()
//左右填充一个节点
let points = [_points[0], ..._points, _points[_points.length - 1]]
//格式化贝塞尔曲线的点
let bezierPoints = []
for (let i = 2; i < points.length - 1; i++) {
bezierPoints.push({
dx: points[i].x,
dy: points[i].y,
cp1x: points[i-1].x + (points[i].x - points[i-2].x)/6,
cp1y: points[i-1].y + (points[i].y - points[i-2].y)/6,
cp2x: points[i].x - (points[i+1].x - points[i-1].x)/6,
cp2y: points[i].y - (points[i+1].y - points[i-1].y)/6,
})
}
return bezierPoints
}
本示例中的贝赛尔曲线的控制点确定如上所示。
绘制曲线
/**
* 绘制一条平滑的曲线
*/
initChart.prototype.drawBezier = function() {
let bezierPoints = this._getBezierPoints()
let points = this._getPoints()
let self = this
let gap = this._xLength / this.xAxisLable.length
// 绘制贝塞尔曲线
this.context.beginPath()
this.context.moveTo(points[0].x, points[0].y)
bezierPoints.forEach(function(item) {
self.context.bezierCurveTo(item.cp1x, item.cp1y, item.cp2x, item.cp2y, item.dx, item.dy)
})
self.context.strokeStyle = '#1abc9c'
self.context.strokeWidth = 4
self.context.stroke()
// 绘制辅助点和线
this.data.forEach(function(item, index) {
let y = self.chartZone[3] - item * self._yLength / self.yMax
let x = self.chartZone[0] + (index + 0.5) * gap
// 绘制一个纵向的辅助线
self.context.beginPath()
self.context.moveTo(x, y)
self.context.lineTo(x, self.chartZone[3])
self.context.setLineDash([8, 8])
self.context.strokeStyle = '#aeaeae'
self.context.strokeWidth = 2
self.context.stroke()
self.context.setLineDash([])
// 绘制一个圆点来标记数据点
self.context.beginPath()
self.context.arc(x, y, 5, 0, 2 * Math.PI, false)
self.context.fillStyle = '#1abc9c'
self.context.fill()
})
return this
}
绘制曲线的代码如上所示,此处使用了两次遍历来绘制,这样来写,对比折线的绘制的一次遍历方式,逻辑结构上更加清晰。
总结
在绘制折线图的时候碰到了一些问题,比如在绘制线条的时候,由于可能开始绘制其他部分修改了画笔颜色,未修改回来会导致颜色不对。为了避免出现这种问题,可以在每次绘制前都设置画笔颜色等属性,以确保不会出错。
扩展思考
除开上述的折线和曲线图以外,还有使区域带有颜色,如Echarts
中的图
这种图又该如何实现这种图形的绘制呢?其实如上的方式也可以很方便来绘制迟来,除了绘制折线以外,我们还需要重新绘制一次折线,再将其和x轴围成一个封闭区域,然后对该区域填充一个颜色即可。此时会使用到closePath
和fill
这两个方法,有兴趣的可以去尝试下。
在向下扩展,如果有多条线和多个堆叠的区域又该如何去绘制呢?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!