小程序绘制饼状图

小程序绘制饼状图,并为每个份额添加对应的描述文字。使用画布canvas,通过js代码实现图文绘制。官方说API wx.createCanvasContext已经停止更新,要求用新API canvas.getContext('2d'),但这个新接口我试了一下,画出来的东西都不是我想要的东西,就只能继续用旧接口了。
示例效果:

pie.jpg

const app = getApp()
Page({

  /**
   * 页面的初始数据
   */
  data: {},

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    // let _this = this
    // wx.createSelectorQuery()
    // .select('#canvas1')
    // .node()
    // .exec(res => {
    //   _this.drawCanvas(res[0].node)
    // })

    
    this.drawCanvas()
  },

  drawCanvas(canvas) {
    // let ctx = canvas.getContext('2d')
    
    let ctx = wx.createCanvasContext('myCanvas')
    // 长-宽 配置参数
    let screenW = app.globalData.screenWidth
    let r90 = this.width_of_px_from_rpx(90)
    let r_radius = this.width_of_px_from_rpx(160)

    // 圆饼圆心坐标
    let circle_center = {x: screenW/2, y: r_radius + r90}
    let pi = Math.PI
    // 比例份额参数
    let shares = [{share: 26, name: '银行卡'},
                  {share: 6, name: '支付分'},
                  {share: 6, name: '微信支付'}, 
                  {share: 8, name: '钱包余额'},
                  {share: 22, name: 'HI'},
                  {share: 25, name: 'zarc'},
                  {share: 3, name: '!'},]
    // 可以对 shares进行处理,把份额少于某个值(如5%)的,合并成一个值'其它',防止分区太小导致显示的文字重叠。
    // 数据的处理非常必要,因为实际数据是计算出来的,难免有浮点数,总值有可能超过100%,如100.0000000000000000001。
    // 实际超过100%也能绘制(只要能使shareCheck() = true),只是后面绘制的会前面绘制的区域。


    // shares = [{share: 92, name: '微信支付'}, 
    // {share: 8, name: '钱包余额'},]
    
    // 绘制-背景圆
    ctx.beginPath()
    ctx.fillStyle = '#d0d0d0'
    ctx.moveTo(circle_center.x, circle_center.y)
    ctx.arc(circle_center.x, circle_center.y, r_radius, 0, pi*2, false)
    ctx.closePath()
    ctx.fill()

    // ----------------------- 绘制-比例份额 -----------------------
    if (!this.shareCheck(shares)) {
      return
    }
    let beginAngle = -pi/2  // 从y轴正轴开始,顺时针旋转绘制份额。如果是0,则从x轴正轴开始顺时针绘制。也可以是任意[无意义]数值,如998
    for (let i = 0; i < shares.length; i++) {
      let share = shares[i]
      let shareAngle = pi*2*(share.share)/100
      let endAngle = beginAngle + shareAngle
      ctx.beginPath()
      ctx.fillStyle = this.colorForShareAt(i)
      // 圆弧绘制
      ctx.moveTo(circle_center.x, circle_center.y)
      ctx.arc(circle_center.x, circle_center.y, r_radius, beginAngle, endAngle, false)
      ctx.closePath()
      ctx.fill()
      // leading_line -> lline 引线绘制
      let centerAngle = beginAngle + shareAngle/2
      // 引线和圆弧接触点(计算触点相对于圆心的x,y偏移距离)
      let point0_Offset = this.x_y_offset_by_angle(centerAngle, r_radius)
      // console.log(point0_Offset)
      ctx.beginPath()
      ctx.strokeStyle = this.colorForShareAt(i)
      // 圆和引线的接触点
      let point0 = {x: circle_center.x + point0_Offset.x, y: circle_center.y + point0_Offset.y}
      ctx.moveTo(point0.x, point0.y)
      // 斜线偏移幅度
      let lline_x_offset_0 = this.width_of_px_from_rpx(20)
      let lline_y_offset_0 = this.width_of_px_from_rpx(30)
      // 折点到文字的距离
      let lline_text_space = this.width_of_px_from_rpx(20)
      // 文字
      let fontPX = Math.floor(this.width_of_px_from_rpx(22))
      ctx.font = 'normal ' + fontPX + 'px 微软雅黑'
      let text = share.name  + ':' + share.share + '%'
      let line_text_w = ctx.measureText(text).width
      // let line_text_max_w = this.width_of_px_from_rpx(130)

      let point1 = {x: 0, y: 0}
      let point2 = {x: 0, y: 0}
      let point_text = {x: 0, y: 0}
      if (point0_Offset.x >= 0) {
        point1.x = point0.x + lline_x_offset_0
        point_text.x = point1.x + lline_text_space
        point2.x = point_text.x + line_text_w
      } else {
        point1.x = point0.x - lline_x_offset_0
        point_text.x = point1.x - lline_text_space - line_text_w
        point2.x = point_text.x
      }
      if (point0_Offset.y >= 0) {
        point1.y = point0.y + lline_y_offset_0
      } else {
        point1.y = point0.y - lline_y_offset_0
      }
      point_text.y = point1.y - fontPX*0.5    // 0.5是调试出来的,*1的话,文字和引线有很大间距。
      point2.y = point1.y
      ctx.fillText(text, point_text.x, point_text.y)

      ctx.lineTo(point1.x, point1.y)
      ctx.lineTo(point2.x, point2.y)
      
      ctx.stroke()

      //
      beginAngle = endAngle
    }
    ctx.draw()
  },

  colorForShareAt(index) {
    let colors = ['#5bb1ec', '#b4a1dd', '#2ec6c6', '#d6897f', '#CD853F', '#9370DB', '#FF6A6A', '#B22222']
    if (index < colors.length) {
      return colors[index]
    } else {
      return '#d0d0d0'
    } 
  },

  // 合计不能超过100%(如果你不想限制--允许重叠覆盖,可以直接 return true)
  shareCheck(shares) {
    if (!shares || shares.length == 0) {
      return false
    }
    let total = 0
    for (let i = 0; i < shares.length; i++) {
      let share = shares[i] 
      total = total + share.share
    }
    console.log('=========================' + total)
    return Math.floor(total) <= 100
  },

  /* angle:弧度, radius:半径
  第一象限: 3/PI*2 < angle < 2*PI
  第二象限:PI < angle < 3/PI*2
  第三象限:PI/2 < angle < PI
  第四象限:0 < angle < PI/2
  返回结果:相对于圆心点的偏移值
  */
  x_y_offset_by_angle(angle, radius) {
    // console.log('radius = ' + radius)
    let simpleAngle = angle
    if (simpleAngle == undefined) {
      return {x: 0, y: 0}
    }
    let pi = Math.PI
    // 将 angle 转换为 0~2*PI之间的值
    if (simpleAngle < 0) {
      let times = Math.floor(simpleAngle/(2*pi))
      let absTimes = Math.abs(times)
      simpleAngle = simpleAngle + (2*pi)*absTimes
    } else if (simpleAngle >= (2*pi)) {
      let times = Math.floor(simpleAngle/(2*pi))
      simpleAngle = simpleAngle - (2*pi)*times
    }
    console.log('simpleAngle = ' + simpleAngle*180/pi)
    // ========================
    // 注意,计算机屏幕上,y 坐标和几何中坐标是相反的,屏幕左上角 y = 0,所以第 1,2象限 offset.y是小于0的。
    let offset = {x: 0, y: 0}
    if (simpleAngle == 0) {
      // x 轴上, x = r
      offset.x = radius
    } else if (simpleAngle < pi/2) {
      // 第 4 象限
      offset.x = radius*Math.cos(simpleAngle)
      offset.y = radius*Math.sin(simpleAngle)
    } else if (simpleAngle == pi/2) {
      // y 轴上, y = r
      offset.y = radius
    } else if (simpleAngle < pi) {
      // 第 3 象限
      let cAngle = simpleAngle - pi/2
      offset.x = -radius*Math.sin(cAngle)
      offset.y = radius*Math.cos(cAngle)
      // console.log('3 cAngle = ' + cAngle*180/pi)
    } else if (simpleAngle == pi) {
      // x 轴上,x = r
      offset.x = -radius
    } else if (simpleAngle < 3*pi/2) {
      // 第 2 象限
      let cAngle = simpleAngle - pi
      offset.x = -radius*Math.cos(cAngle)
      offset.y = -radius*Math.sin(cAngle)
      // console.log('2 cAngle = ' + cAngle*180/pi)
    } else if (simpleAngle == 3*pi/2) {
      // y 轴上, y = -r
      offset.y = -radius
    } else {
      // 第 1 象限
      let cAngle = simpleAngle - 3*pi/2
      offset.x = radius*Math.sin(cAngle)
      offset.y = -radius*Math.cos(cAngle)
      // console.log('1 cAngle = ' + cAngle*180/pi)
    }
    return offset
  },

  onReady: function () {},
  onShow: function () {},
  onHide: function () {},
  onUnload: function () {},
  onPullDownRefresh: function () {},
  onReachBottom: function () {},
  onShareAppMessage: function () {},

  // 将 rpx 单位长度换算成 px。因为canvas只能接收 px,
  width_of_px_from_rpx(value) {
    let screenW = app.globalData.screenWidth
    let value_of_px = value/750.0*screenW
    return value_of_px
  }
})

关于app.globalData.screenWidth,app.js onLaunch()内已经初始化

const systemInfo = wx.getSystemInfoSync();
this.globalData.screenWidth = systemInfo.screenWidth

wxml


  
  

wxss

.myPage {
  width: 100vmin;
  min-height: 100vmax;
  position: relative;
  z-index: 0;
}

.rowh40 {width: 100%;height: 40rpx;}
说明

手机屏幕本身小,所以canvas我设置width = 750rpx;即屏幕宽度。
每个份额的颜色是 colorForShareAt(index)根据index来配的,你可以随便改其他条件。
可修改的配置参数

let r90 = this.width_of_px_from_rpx(90)    饼图离画布顶部的距离
let r_radius = this.width_of_px_from_rpx(160)    饼图半径

 圆饼圆心坐标
let circle_center = {x: screenW/2, y: r_radius + r90}
let beginAngle = -pi/2  
(引线的)斜线  
      偏移幅度
      let lline_x_offset_0 = this.width_of_px_from_rpx(20)  表示(引线的)斜线两个端点在x轴方向的间距为 20 rpx
      let lline_y_offset_0 = this.width_of_px_from_rpx(30)
      折点到文字的距离
      let lline_text_space = this.width_of_px_from_rpx(20)
      文字
      let fontPX = Math.floor(this.width_of_px_from_rpx(22))
可以改进的地方:

饼图上标记的文字,如果每个份额比较平均,都比较大时,显示出来是优雅的。
但是,如果每个份额很小,类型很多,比如都是1%,2%,3%这种扎堆了,就会导致文字重叠。
如果是电脑网页,可以往左右扩展(或上下扩展),但是手机屏幕就一点点,就算优化,效果也有限。所以我觉得没有在绘制过程进行优化(当然你仍然可以去优化一下)。
我认为最简单方案,就是如果小份额(如5%或3%以下份额,)有连续2个以上值的时候,则可以把这些份额全部合并到'其它'类型份额去,这样看起来更直观清晰,也能避免文字重叠的问题。
思路就是优化数据,而不是去优化布局。

你可能感兴趣的:(小程序绘制饼状图)