微信小程序canvas画价格走势图(五)

最近比较忙,所以趁着今年的最后一个周末,来把最后两篇完结掉。这是倒数第二篇了,这一篇的内容很简单,讲的是细节的绘制,也就是底部的刻度线和日期的绘制,以及一个小小的总结。

目前我们完成了canvas的准备、折线图的绘制、价格圆点的绘制、价格和拼团人数的标注,可以说是几乎已经完成了,目前效果是这样的——

也就是,下方的横轴上是光秃秃的,我们现在来加上合适的内容。

一、刻度线和日期的绘制

如你所见,图上有灰色的坐标轴。因为它带有箭头,用canvas画出来比较困难,而且又不重要,只是个背景板,不如直接用图片做吧,所以我是放了个图在canvas的下面一层。这个横轴之前还是有自带一些刻度线的,我只好用PS把这样线给P掉,把底部还原成光滑直线才进行这一步。

绘制刻度线要解决一个问题,那就是绘制的位置

为什么会有刻度线,肯定是因为这里有拐点吧,所以刻度线的中心部位与拐点的x坐标相同,那么在知道拐点坐标后,就知道了刻度线的x位置。另外,特殊点下方这里没有刻度线,所以我只会在非特殊点的时候来绘制刻度。还要注意一个细节,那就是刻度线的粗细。所以刻度线最左边的x坐标并不等于拐点的x坐标,而是等于拐点x坐标再减去刻度线的粗细的一半,它最右边的坐标等于拐点x坐标再加上刻度线粗细的一半

刻度线的y坐标在哪里呢?显然底部位于横轴上,顶部的y等于横轴y减去刻度线的高度,并且所有的刻度线y值相同。所以底部的y是多少呢?canvas的高度吗?不不不,由于刻度线下面还要写日期,所以canvas下方还留了一定的空间,所以底部的y就是canvas的高度减去底部空间的高度

刻度线虽然很细,但我仍然把它当成一个实心矩形,所以我是找好了这4个点(左上,右上,右下,左下)的坐标,围成一个矩形,然后填充颜色的。

因为我定义的刻度线宽度为4rpx,高度为16rpx,颜色为#eee,所以我绘制刻度线的函数是这样的——

/**
     * 画出坐标轴的刻度
     * @param {*} context 上下文对象
     * @param {*} center 拐点信息
     * @param {*} height 画布高度
     */
    function drawMark(context, center, height) {
      context.beginPath();
      console.log(height)
      // 底部显示文字区域的高度
      const bottomPos = 72 * _this.data.toPx;
      context.moveTo(center.x - 2 * _this.data.toPx, height - (bottomPos + 16 * _this.data.toPx));
      context.lineTo(center.x + 2 * _this.data.toPx, height - (bottomPos + 16 * _this.data.toPx));
      context.lineTo(center.x + 2 * _this.data.toPx, height - bottomPos);
      context.lineTo(center.x - 2 * _this.data.toPx, height - bottomPos);
      context.closePath()
      context.fillStyle = "#eaeaea"
      context.fill();
      context.draw(true);
    }

日期的绘制比较简单,只要注意日期与刻度线水平居中对齐就好了。也就只有一个要点,日期的中间位置与拐点坐标x相同。所以只需要在设置好日期的字体样式后量取文字的宽度,把字体起点的x坐标设为拐点x坐标减去文字宽度的一半即可

代码是这样的——

 /**
    * 画出刻度线下的日期
    * @param {*} context 上下文对象
    * @param {*} center 拐点信息
    * @param {*} height 画布高度
    */
    function drawDate(context, center, height) {
      // 写出刻度对应的日期
      context.beginPath();
      context.setFontSize(28 * _this.data.toPx);
      context.fillStyle = '#333333';
      // 计算文字的宽度
      var textWidth = getTextWidth(context, center.date)
      // 确保文字显示在刻度的正中间
      context.fillText(center.date, center.x - (textWidth / 2), height - 30 * _this.data.toPx);
      context.draw(true);
    }

二、原价和箭头的绘制

可以说这个是非常微不足道的细节了,仅仅只是在左上角画几个字和一个图而已,需要用到的方法是context的fillText和drawImage这两个

需要注意的地方是原价的对齐问题drawImage的参数

原价的左上角位置很好找,在图上两个差不多的地方就可以了。注意一下,原价文字的水平方向的中点在哪里,因为箭头与文字要水平居中对齐。显然,应该是原价绘制起点的x坐标加上原价文字宽度的一半。

drawImage的第一个参数是要绘制的图片,如果是本地图片只需要写路径即可,如果是在线图片需要将图片下载下来才可以绘制,我这里用的就是本地图片。第二和第三个参数是要绘制图片左上角的地方,第四个和第五个参数是图片要画多大。我就使用了前五个参数绘制图片左上角的y显然比绘制原价文字的y要大一些,那么x坐标呢?显然并不是等于之前的文字中心点,而是在文字中心点往左平移一半图片宽度的地方

解决完这两个问题,答案就呼之欲出了。

 /** 
     * 写出原价,返回原价的宽度
     * @param {*} context 上下文对象
     * @param {*} originalPrice 原价
     * @param {*} x 原价的横坐标,单位是px
     * @param {*} y 原价的纵坐标,单位是px
     */
    function drawOriginPrice(context, originalPrice, x, y) {
      context.beginPath();
      context.fillStyle = '#333333';
      context.font = 'normal bold 16px sans-serif';
      context.fillText('原价' + originalPrice, x, y);
      context.draw(true);
      // 计算并返回原价的宽度
      return getTextWidth(context, '原价' + originalPrice)
    } 

/**
     * 绘制图片,如这里的红色箭头
     * @param {*} context 上下文对象
     * @param {*} imgPath 要绘制的图片路径
     * @param {*} x 图片最左边的坐标,单位是px
     * @param {*} y 图片最上边的坐标,单位是px
     * @param {*} imgWidth 图片宽度,单位是rpx
     * @param {*} imgHeight 图片高度,单位是rpx
     */
    function drawImage(context, imgPath, x, y, imgWidth, imgHeight) {
      context.drawImage(imgPath, x, y, imgWidth *_this.data.toPx, imgHeight*_this.data.toPx);
      context.draw(true);
    }

三、主函数

其实我是在绘制完所有东西后,才拆分成主函数和子函数的。这样拆分的好处有几个:

1.让我们调用函数的时候只需要跟主函数的打交道,所以调用的时候只需要考虑主函数的参数即可,也就是只需要传入我们的数据、canvas边界相对于绘制折线图区域的边界四个方向上的距离(如果不留,那么会紧贴这canvas的边缘开始绘图)和特殊点的下标即可。这些数据都是比较直观的,很容易理解。

2.让流程更清晰。在主函数里面,我们就按照流程的先后顺序,去调用执行相关功能的子函数了,在主函数里面,用一行代码就可以代表一个流程。我把流程分为4大步(绘制折线图、绘制拐点、绘制原价和绘制箭头),第二步又分成几个小步,每一小步都是调用某个子函数。

3.让调试更方便。现在不再是一大坨代码放在一个函数里了,而是让这些执行功能的子函数获取主函数传入的参数,并把在其他子函数会使用的东西返回给主函数。哪个流程如果出问题,看结果就知道是哪个子函数的问题了。

拆分的过程我也说一下:

1.在主函数中留下我们传入的参数,与子函数需要的数据。还要留下主流程,比如绘制所有的拐点,以及判断当前是否为特殊点这些属于主流程,而不是单独的子功能。然后观察一下,判断要拆分出几个子函数

2.分析哪些代码属于某个子功能,为它创建对应的子函数,并且把这一段代码直接拷到子函数中。

3.判断一下,这个子函数使用到了哪些数据,这些数据怎么来的,要么是来自页面的data,要么是来自主函数的传参。因为子函数都是在绘图,必须使用context,所以我规定第一个参数都为context,之后的参数就根据需要,让主函数传入。所以我定义了相应的形参,并写到了函数的文档注释中。

4.如果子函数得出了在其他子函数里也需要的数据,那么要把这个数据作为子函数的返回值返回给主函数。比如绘制折线图的函数得到了拐点数据,这是几乎每个子函数都要使用的哦东西,必须返回给主函数才可以正常绘图。

5.对每个独立的功能都这么做,然后在主函数这边以合理的顺序调用这些子函数即可

最后,只需要准备好相应的数据,在onLoad的时候调用主函数即可完成绘制!

最后贴一下,主函数的代码。

/**
     * 这个是绘图的主函数
     * @param {*} arr 每个值组成的数组
     * @param {*} xLeft 折线图最左边与画布边界的距离,单位是px,默认为0
     * @param {*} xRight 折线图最右边与画布边界的距离,单位是px,默认为0
     * @param {*} yBottom 折线图最下边与画布边界的距离,单位是px,默认为0
     * @param {*} yTop 折线图最上边与画布边界的距离,单位是px,默认为0
     * @param {*} specialIndex 特殊点的下标,默认为-1(默认没有特殊点)
     */
    function draw(arr, xLeft = 0, xRight = 0, yBottom = 0, yTop = 0, specialIndex = -1) {
      // 获取上下文对象
      const context = wx.createCanvasContext('canvas');
      // 获取画布的宽高
      const width = _this.data.canvasWidth;
      const height = _this.data.canvasHeight;
      // 记录图形的边界
      const borderInfo = { xLeft, xRight, yBottom, yTop }
      // 1.画折线图,传入数据,主色调,画布宽高,图形边界和特殊点位置
      // 获得返回的拐点数组
      const pointArray = drawFoldLine(context, arr, mainColor, width, height, borderInfo, specialIndex)

      // 2.遍历拐点数组
      for (let i = 0; i < pointArray.length; i++) {
        // 把拐点作为圆心
        let center = pointArray[i];
        // 2.1画出小圆
        drawInflectionPoint(context, mainColor, center, 14, i === specialIndex);

        if (i !== specialIndex) {// 不是特殊点的情况
          // 2.2 写出价格
          drawNormalPrice(context, center);
          // 2.3写拼团人数
          drawNormalTeamNumber(context, center);
          // 2.4画出下面的刻度
          drawMark(context, center, height);
          // 2.5 画出刻度线下面的日期
          drawDate(context, center, height);
        }
        else {//是特殊点的情况
          // 2.6写出特殊情况的价格
          drawSpecialPrice(context, center);
          // 2.7写出特殊情况的拼团人数
          drawSpecialTeamNumber(context, center, mainColor);
        }
      }
      // 3.在图上写出原价
      // 得到原价的宽度
      const priceTextWidth = drawOriginPrice(context, originalPrice, 60 * _this.data.toPx, 40 * _this.data.toPx);
      console.log(priceTextWidth)
      // 4.画出下降的箭头,传入箭头路径,位置和大小(位置的单位是px,大小的单位是rpx)
      // 箭头的x位置是:从原价的最左边右移原价宽度的一半,再左移箭头宽度的一半
      const imgX = 60 * _this.data.toPx + priceTextWidth / 2 - 78 * _this.data.toPx / 2;
      drawImage(context, arrowPic, imgX, 56 * _this.data.toPx, 78, 112)
    }

数据的示例:

  const arr = [
      { price: 9.9, number: 2, date: '12.10' },
      { price: 0.99, number: 5, date: '' },
      { price: 19.9, number: 2, date: '12.17' },
      { price: 29.9, number: 2, date: '1.02-1.19' }];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值