1. 程式人生 > >微信小程式canvas繪製雷達圖

微信小程式canvas繪製雷達圖

效果圖展示:

程式碼實現(具體實現):

const DEFAULT_COLOR = '#FC9A00';

class Radar {
  constructor(options) {
    const {
      canvasId,
      width,
      height,
      categories,
      data,
      initrad = Math.PI,
      colors = []
    } = options;

    this.colors = colors;

    this.categories = categories;
    // 直徑
    const diameter = Math.min(width, height);
    this.ctx = wx.createCanvasContext(canvasId);
    // 圓心
    this.center = { x: width / 2, y: height / 2 };
    // 半徑
    this.radius = diameter / 2;
    if (categories.length) {
      // 減去文字的高度 先定為20
      this.radius -= 30;
    }
    // 幾邊形
    this.step = data.length;

    const maxData = Math.max(...data);
    if (maxData == 0) {
      this.rateArray = data;
    } else {
      this.rateArray = data.map(d => d / maxData);
    }
    // this.rateArray = data; // 值

    // 開始的角度
    this.initrad = initrad;

    // 清空上次畫的內容
    this.ctx.draw();

    this.drawBackground(); // 背景
    this.drawRate(); // 畫比例
    this.drawBackLine(); // 畫白色的線
    this.drawRateLine(); // 畫中間的線
    this.drawCenter(); // 畫圓心
    this.drawRatePoint(); // 畫點
    this.drawText(); // 畫文字
  }

  // 畫背景
  drawBackground() { // 測試畫背景線
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;

    // 畫蜘蛛網
    var isBlue = false;
    for (var s=8; s>0; s--) {
      this.ctx.beginPath();
      this.ctx.setLineWidth(1);
      this.ctx.setStrokeStyle('#f8f8f8');
      let lineWidth = this.radius / 6;
      for (var i=0;i<this.step;i++) {
        initrad += rad;
        this.ctx.setLineWidth(lineWidth);
        const x = this.center.x + Math.sin(initrad) * (this.radius - lineWidth / 2)*(s/8);
        const y = this.center.y + Math.cos(initrad) * (this.radius - lineWidth / 2)*(s/8);
        this.ctx.lineTo(x, y);
      }
      if (s>6) {
        isBlue = !isBlue;
      }
      this.ctx.closePath();
      if (isBlue) {
        this.ctx.setLineJoin('round');
        this.ctx.stroke();
      }
    }

    // 畫內圈線
    isBlue = false;
    for (s=8; s>0; s--) {
      this.ctx.beginPath();
      this.ctx.setLineWidth(0.5);
      this.ctx.setStrokeStyle('#eeeeee');
      for (i=0;i<this.step;i++) {
        initrad += rad;
        const x = this.center.x + Math.sin(initrad) * this.radius*(s/8) * 0.66;
        const y = this.center.y + Math.cos(initrad) * this.radius*(s/8) * 0.66;
        this.ctx.lineTo(x, y);
      }
      if (s>6) {
        isBlue = !isBlue;
      }
      this.ctx.closePath();
      if (isBlue) {
        this.ctx.setLineJoin('round');
        this.ctx.stroke();
      }
    }
    this.ctx.draw(true);
  }

  // 畫比例
  drawRate() {
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;
    const initPoint = {
      x: this.center.x + Math.sin(initrad) * this.radius * this.rateArray[0],
      y: this.center.y + Math.cos(initrad) * this.radius * this.rateArray[0]
    };
    this.ctx.moveTo(initPoint.x, initPoint.y);
    this.ctx.beginPath();
    this.ctx.setStrokeStyle('#feeeb8');
    this.ctx.setLineWidth(0.5);
    let isShowIf = false;
    let arr = []; // 點數
    let arr1 = [];
    let arr2 = [];
    for (let i=0;i<this.rateArray.length;i++) {
      if (this.rateArray[i]) {
        arr.push(1);
      }
      let index = i;
      let index_ = i + 1;
      let index2 = i + 2;
      let index4 = i + 4;
      let index5 = i + 5;
      if (index2 > (this.step-1)) index2 = index2 - this.step;
      if (index4 > (this.step-1)) index4 = index4 - this.step;
      if (index5 > (this.step-1)) index5 = index5 - this.step;

      if (this.rateArray[index] && this.rateArray[index_]) {
        arr1.push(1);
      }
      if (this.rateArray[index] && this.rateArray[index_] && this.rateArray[index2]) {
        arr2.push(1);
      }
      if (this.rateArray[index] && this.rateArray[index4] && this.rateArray[index5]) {
        arr2.push(1);
      }
      if (this.rateArray[index] && this.rateArray[index4]) {
        arr1.push(1);
      }
      if (this.rateArray[index] && this.rateArray[index5]) {
        arr1.push(1);
      }
    }
    if (arr.length == 2 && arr1.length) isShowIf = true;
    if (arr.length == 2 && !arr1.length) isShowIf = false;
    if (arr.length == 3 && arr2.length) isShowIf = true;
    if (arr.length == 3 && !arr2.length) isShowIf = false;
    if (arr.length == 4) isShowIf = false;

    for (let i = 0; i < this.step; i++) {
      const x = this.center.x + Math.sin(initrad) * this.radius * this.rateArray[i];
      const y = this.center.y + Math.cos(initrad) * this.radius * this.rateArray[i];
      if (isShowIf) {
        this.ctx.lineTo(x, y);
        this.ctx.stroke();
      } else {
        if (!(x === this.center.x && y === this.center.y)) { // 不連線圓心
          this.ctx.lineTo(x, y);
          this.ctx.stroke();
        }
      }
      initrad += rad;
      var grd = this.ctx.createCircularGradient(this.center.x, this.center.y, this.radius);
      grd.addColorStop(0, '#feeeb8');
      grd.addColorStop(1, '#feeeb8');
    }
    this.ctx.lineTo(initPoint.x, initPoint.y);
    this.ctx.setFillStyle(grd);
    this.ctx.fill();
    this.ctx.stroke();
    this.ctx.closePath();
    this.ctx.draw(true);
  }

  // 畫中間的線
  drawRateLine() {
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;
    const initPoint = {
      x: this.center.x + Math.sin(initrad) * this.radius * this.rateArray[0],
      y: this.center.y + Math.cos(initrad) * this.radius * this.rateArray[0]
    };
    this.ctx.setStrokeStyle('#F2E9B7');
    this.ctx.setLineWidth(0.5);
    this.ctx.moveTo(this.center.x, this.center.y);
    this.ctx.lineTo(initPoint.x, initPoint.y);
    this.ctx.stroke();
    for (let i = 0; i < this.step; i++) {
      const x = this.center.x + Math.sin(initrad) * this.radius * this.rateArray[i];
      const y = this.center.y + Math.cos(initrad) * this.radius * this.rateArray[i];
      this.ctx.moveTo(this.center.x, this.center.y);
      this.ctx.lineTo(x, y);
      this.ctx.stroke();
      initrad += rad;
    }
    this.ctx.draw(true);
  }

  // 畫點
  drawRatePoint() {
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;
    const initPoint = {
      x: this.center.x + Math.sin(initrad) * this.radius * this.rateArray[0],
      y: this.center.y + Math.cos(initrad) * this.radius * this.rateArray[0]
    };
    this.ctx.beginPath();
    this.ctx.arc(initPoint.x, initPoint.y, 2, 0, 2 * Math.PI);
    this.ctx.setFillStyle('#ffffff');
    this.ctx.fill();
    this.ctx.closePath();

    let color = this.colors[0] || DEFAULT_COLOR;

    this.ctx.beginPath();
    this.ctx.arc(initPoint.x, initPoint.y, 1, 0, 2 * Math.PI);
    this.ctx.setFillStyle(color);
    this.ctx.fill();
    this.ctx.closePath();
    for (let i = 0; i < this.step; i++) {
      const x = this.center.x + Math.sin(initrad) * this.radius * this.rateArray[i];
      const y = this.center.y + Math.cos(initrad) * this.radius * this.rateArray[i];

      let color = this.colors[i] || DEFAULT_COLOR;

      this.ctx.beginPath();
      this.ctx.arc(x, y, 2, 0, 2 * Math.PI);
      this.ctx.setFillStyle('#ffffff');
      this.ctx.fill();
      this.ctx.closePath();

      this.ctx.beginPath();
      this.ctx.arc(x, y, 2, 0, 2 * Math.PI);
      this.ctx.setFillStyle(color);
      this.ctx.fill();
      this.ctx.closePath();
      initrad += rad;
    }
    this.ctx.draw(true);
  }

  // 畫白色的線
  drawBackLine() {
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;
    this.ctx.setStrokeStyle('#eeeeee');
    this.ctx.setLineWidth(0.5);
    for (let i = 0; i < this.step; i++) {
      this.ctx.moveTo(this.center.x, this.center.y);
      const x = this.center.x + Math.sin(initrad) * this.radius;
      const y = this.center.y + Math.cos(initrad) * this.radius;
      this.ctx.lineTo(x, y);
      this.ctx.stroke();
      initrad += rad;
    }
    this.ctx.draw(true);
  }

  // 畫圓心
  drawCenter() {
    this.ctx.beginPath();
    this.ctx.arc(this.center.x, this.center.y, 1, 0, 2 * Math.PI);
    // this.ctx.setFillStyle('rgb(205, 241, 250)');
    this.ctx.setFillStyle('#cccccc');
    this.ctx.fill();
    this.ctx.draw(true);
    this.ctx.closePath();
  }

  // 繪製文字
  drawText() {
    const rad = 2 * Math.PI / this.step;
    let initrad = this.initrad;
    this.ctx.setFontSize(13);
    this.ctx.setFillStyle('#333333');
    this.ctx.setTextBaseline('middle');
    this.ctx.setTextAlign('center');
    for (let i = 0; i < this.step; i++) {
      const x = this.center.x + Math.sin(initrad) * this.radius;
      const y = this.center.y + Math.cos(initrad) * this.radius;
      const text = this.categories[i];

      // 偏移值
      let incX = 0, incY = 0;

      let offsetX = x - this.center.x;
      let offsetY = y - this.center.y;

      // 水平方向上左右兩邊
      if (offsetX > 40) {
        incX = 40;
      } else if (offsetX < -40) {
        incX = -40;
      }

      // 垂直方向上距離雷達圖
      if (offsetY > 40) {
        incY = 5;
      } else if (offsetY < -40) {
        incY = -5;
      }

      // 上下兩個相鄰元素的間距
      if (this.step == 6) {
        if (i === 4) incX = -40;
        if (i === 3) incX = 40;
        if (i === 1) incX = 40;
        if (i === 0) incX = -40;
      }

      let color = this.colors[i] || '#333333';

      // 當文字長度大於4時,分成兩列
      if (text.length > 4) {
        let firstLine = text.slice(0, 4);
        let secondLine = text.slice(4);
        if (text.slice(0, 5) == 'STEAM') {
          firstLine = text.slice(0, 7);
          secondLine = text.slice(7);
        }
        if (incY < 0) incY = -20;
        if (incY > 0) incY = -5;
        if (incY == 0) incY = -10;

        this.ctx.setFillStyle(color);
        this.ctx.setFontSize(18);
        this.ctx.fillText(secondLine, x + incX, y + incY);

        this.ctx.setFontSize(12);
        this.ctx.setFillStyle('#666');
        this.ctx.fillText(firstLine, x + incX, y + incY + 18);
        this.ctx.setTextAlign('center');
      } else {
        this.ctx.fillText(text, x + incX, y + incY);
      }

      initrad += rad;
    }
    this.ctx.draw(true);
  }
}

module.exports = Radar;

呼叫:

// 畫雷達圖
  drawRadar(titleArr, countArr) {
    let initrad = undefined;
    if (countArr.length == 4) {
      initrad = Math.PI * 2;
    } else if (countArr.length == 5) {
      initrad = -Math.PI * 7 / 36;
    } else {
      initrad = Math.PI * 11 / 6;
    }

    const colors = [
      '#5bc5be',
      '#fcb046',
      '#f26747',
      '#f5457e',
      '#786eff',
      '#03c3fb'
    ];

    new Radar({
      canvasId: 'radarCanvas',
      colors,
      width: 260,
      height: 180,
      categories: titleArr.reverse(), // ['故事回本', '英語教育', ...],分類陣列
      data: countArr.reverse(), // [3, 5, ...], 分類對應的值
      initrad: initrad,
    });
  },