最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • canvas之旅系列----(二)

    正文概述 掘金(不想做后端的前端不是好运维)   2021-01-06   569

    canvas绘制折线图

    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()
    }
    

    曲线绘制

    在上示例中,我们绘制出来的是一个折线图,如果我们想让线条更加流畅,绘制一条曲线图而非折线图,如下图所示,那么我们该如何做呢?

    canvas之旅系列----(二)

    三次贝塞尔曲线

    如想绘制出上图所示的一条曲线,我们则需要使用到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中的图

    canvas之旅系列----(二)

    这种图又该如何实现这种图形的绘制呢?其实如上的方式也可以很方便来绘制迟来,除了绘制折线以外,我们还需要重新绘制一次折线,再将其和x轴围成一个封闭区域,然后对该区域填充一个颜色即可。此时会使用到closePathfill这两个方法,有兴趣的可以去尝试下。

    在向下扩展,如果有多条线和多个堆叠的区域又该如何去绘制呢?


    起源地下载网 » canvas之旅系列----(二)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元