Broken line

rendering

User-defined component line-chart
<canvas type="2d" id="line" class="line-class" style="width:{{width}}px; height:{{height}}px" />
Copy the code
Component({
  externalClasses: ['line-class'].properties: {
    width: String.height: String.data: Array,},observers: {
    width() {
      // Listen for width changes to redraw the canvas
      // Dynamically passing width seems to be the only way...
      const query = this.createSelectorQuery();
      query
        .select('#line')
        .fields({ node: true.size: true })
        .exec(res= > {
          const canvas = res[0].node;
          const ctx = canvas.getContext('2d');
          const width = res[0].width; // Canvas width
          const height = res[0].height; // Canvas height

          console.log(` width:${width}, height:${height}`);

          const dpr = wx.getSystemInfoSync().pixelRatio;
          canvas.width = width * dpr;
          canvas.height = height * dpr;
          ctx.scale(dpr, dpr);

          // Start drawing
          this.drawLine(ctx, width, height, this.data.data); }); }},methods: {
    drawLine(ctx, width, height, data) {
      const Max = Math.max(... data);const Min = Math.min(... data);// Divide the canvas width and height equally according to certain rules
      const startX = width / (data.length * 2), // The x-coordinate of the starting point X
        baseY = height * 0.9.// Baseline Y
        diffX = width / data.length,
        diffY = (height * 0.7) / (Max - Min); // The height is reserved for 0.2 write temperature

      ctx.beginPath();
      ctx.textAlign = 'center';
      ctx.font = '13px Microsoft YaHei';
      ctx.lineWidth = 2;
      ctx.strokeStyle = '#ABDCFF';

      // Draw the line of the fold line
      data.forEach((item, index) = > {
        const x = startX + diffX * index,
          y = baseY - (item - Min) * diffY;

        ctx.fillText(`${item}° `, x, y - 10);
        ctx.lineTo(x, y);
      });
      ctx.stroke();

      // Draw a fold line background
      ctx.lineTo(startX + (data.length - 1) * diffX, baseY); // Baseline endpoint
      ctx.lineTo(startX, baseY); // Baseline starting point
      const lingrad = ctx.createLinearGradient(0.0.0, height * 0.7);
      lingrad.addColorStop(0.'rgba (255255255,0.9)');
      lingrad.addColorStop(1.'rgba(171,220,255,0)');
      ctx.fillStyle = lingrad;
      ctx.fill();

      // Draw the dots on the fold diagram
      ctx.beginPath();
      data.forEach((item, index) = > {
        const x = startX + diffX * index,
          y = baseY - (item - Min) * diffY;

        ctx.moveTo(x, y);
        ctx.arc(x, y, 3.0.2 * Math.PI);
      });
      ctx.fillStyle = '#0396FF'; ctx.fill(); ,}}});Copy the code

Data is a temperature array, such as [1, 2…

Since we don’t know how many temperature values there are, the width here is passed in dynamically

There is a small problem, is that the width is too large the real machine will not display…

 // Get the total width of the scroll view
 wx.createSelectorQuery()
      .select('.hourly')
      .boundingClientRect(rect= > {
        this.setData({
          scrollWidth: rect.right - rect.left,
        });
      })
      .exec();
Copy the code
<view class="title">Hour overview</view>
<scroll-view scroll-x scroll-y class="scroll" show-scrollbar="{{false}}" enhanced="{{true}}">
    <view class="hourly">
      <view wx:for="{{time}}" wx:key="index">{{item}}</view>
    </view>
    <line-chart line-class="line" width="{{scrollWidth}}" height="100" data="{{temp}}" />
</scroll-view>
Copy the code

Here write scroll x and scroll y, will there be absolute positioning offset problem, also do not know why 😭

.scroll {
  position: relative;
  height: 150px;
  width: 100%;
}

.hourly {
  display: flex;
  height: 150px;
  position: absolute;
  top: 0;
}

.hourly > view {
  min-width: 3.5 em;
  text-align: center;
}

.line{// The line chart is definitely positioned at the bottomposition: absolute;
  bottom: 0;
}
Copy the code

According to hourly accounts, the absolute location is used to simulate the effects of a line diagram of inkblot weather and a block in a single day, so the height of hourly accounts is equal to that of a scrollview, and the canvas needs to be located

Is mainly do not know how to achieve ink weather, can only temporarily so

Third order Bezier curve

rendering

Emmm, not exactly slick 🤣

Computational control point

Let’s start with a point class

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y; }}Copy the code

Canvas Bezier Curve Drawing Tool (karlew.com)

From this website you can know the meaning of each parameter of the third-order Bessel curve

So when you use bezierCurveTo the last point is the next point and the first two are the control points

Control point calculation reference: Bezier curve control point determination method – Baidu Library

Condense it down

This a and b could be any positive number

So define A method to compute the control points A and B of A point

/** * Calculates the bezier curve control point of the current point *@param {Point} PreviousPoint: previousPoint *@param {Point} CurrentPoint: indicates the currentPoint *@param {Point} NextPoint1: The next point *@param {Point} NextPoint2: next point *@param {Number} The coefficient of scale: * /
calcBezierControlPoints(
  previousPoint,
  currentPoint,
  nextPoint1,
  nextPoint2,
  scale = 0.25
) {
  let x = currentPoint.x + scale * (nextPoint1.x - previousPoint.x);
  let y = currentPoint.y + scale * (nextPoint1.y - previousPoint.y);

  const controlPointA = new Point(x, y); // Control point A

  x = nextPoint1.x - scale * (nextPoint2.x - currentPoint.x);
  y = nextPoint1.y - scale * (nextPoint2.y - currentPoint.y);

  const controlPointB = new Point(x, y); // Control point B

  return { controlPointA, controlPointB };
}
Copy the code

Here scale is just a and B, but take the same values

But the first point has no previousPoint, and the penultimate point has no nextPoint2

So use currentPoint instead of previousPoint when point is first

When the next-to-last point is reached, use nextPoint1 instead of nextPoint2

As for the last point, you don’t need to do anything because the third parameter bezierCurveTo is the next point. You only need to supply the coordinates to connect the points without calculating the control points

Therefore, the method of drawing third-order Bezier curves is as follows:

/** * Draw bezier curve * CTX. BezierCurveTo (control point 1, control point 2, current point); * /
drawBezierLine(ctx, data, options) {
  const { startX, diffX, baseY, diffY, Min } = options;

  ctx.beginPath();
  // Move to the first point
  ctx.moveTo(startX, baseY - (data[0] - Min) * diffY);

  data.forEach((e, i) = > {
    let curPoint, prePoint, nextPoint1, nextPoint2, x, y;

    / / the current point
    x = startX + diffX * i;
    y = baseY - (e - Min) * diffY;
    curPoint = new Point(x, y);

    // The previous point
    x = startX + diffX * (i - 1);
    y = baseY - (data[i - 1] - Min) * diffY;
    prePoint = new Point(x, y);

    // Next point
    x = startX + diffX * (i + 1);
    y = baseY - (data[i + 1] - Min) * diffY;
    nextPoint1 = new Point(x, y);

    // The next point
    x = startX + diffX * (i + 2);
    y = baseY - (data[i + 2] - Min) * diffY;
    nextPoint2 = new Point(x, y);

    if (i === 0) {
      // If it is the first point, the previous point is replaced by the current point
      prePoint = curPoint;
    } else if (i === data.length - 2) {
      // If it is the penultimate point, the next point is replaced by the next point
      nextPoint2 = nextPoint1;
    } else if (i === data.length - 1) {
      // The last point exits directly
      return;
    }

    const { controlPointA, controlPointB } = this.calcBezierControlPoints(
      prePoint,
      curPoint,
      nextPoint1,
      nextPoint2
    );

    ctx.bezierCurveTo(
      controlPointA.x,
      controlPointA.y,
      controlPointB.x,
      controlPointB.y,
      nextPoint1.x,
      nextPoint1.y
    );
  });

  ctx.stroke();
},
Copy the code

Encapsulate as a class

Go straight to the code…

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y; }}export class Line {
  constructor(chart) {
    this.canvas = chart.node;
    this.ctx = this.canvas.getContext('2d');
    this.width = chart.width;
    this.height = chart.height;

    const dpr = wx.getSystemInfoSync().pixelRatio;
    this.canvas.width = this.width * dpr;
    this.canvas.height = this.height * dpr;
    this.ctx.scale(dpr, dpr);
  }

  /** * Initialize the chart *@param {Object} option* option = { * textStyle: { * color: '#fff', * fontSize: '10px', * fontFamily: 'Microsoft YaHei', * textAlign: 'center', * }, * series: [ * { * data: [820, 932, 901, 934, 1290, 1330, 1320], * smooth: true, * label: { * show: * lineStyle: {width: 3, * color: '#abdcffa', * backgroundColor: 'rgba (171220255,0.9), *}}, {...} * *}; * /
  init(options) {
    this.option = options;
    const series = options.series;

    let data = [],
      maxLength = 0;
    
    series.forEach(el= > {
      if (el.data instanceof Array) {
        data.push(el.data);

        // Get the maximum length of each group of data
        if(maxLength < el.data.length) { maxLength = el.data.length; }}});this.Max = Math.max(... data.flat());/ / Max
    this.Min = Math.min(... data.flat());/ / the minimum

    // Divide the canvas width and height equally according to certain rules
    this.startX = this.width / (maxLength * 2); // The x-coordinate of the starting point X
    this.baseY = this.height * 0.9; // Baseline Y
    this.diffX = this.width / maxLength; // The width difference for each element
    this.diffY = (this.height * 0.7)/(this.Max - this.Min); // High reserved 0.2 write labels

    const textStyle = options.textStyle;
    this.ctx.textAlign = textStyle.textAlign || 'center';
    this.ctx.font = `${textStyle.fontSize || '14px'} ${
      textStyle.fontFamily || 'monospace'
    }`;

    // Start drawing
    series.forEach(el= > {
      if (el.data instanceof Array) {
        if (el.smooth) {
          / / graph
          const path = this.createBezierLine(el);
          this.drawLine(path, el);
        } else {
          / / line chart
          const path = this.createBrokenLine(el);
          this.drawLine(path, el); }}}); }/ / to draw
  drawLine(path, el) {
    const { data, label, lineStyle } = el;

    this.drawBackground(path, el); / / the background

    this.ctx.beginPath();

    this.ctx.lineWidth = lineStyle? .width ||3;
    this.ctx.strokeStyle = lineStyle? .color ||'#abdcff';

    this.ctx.stroke(path);

    this.drawLabel(data, label); / / label
    this.drawDots(data); / / dots
  }

  // Draw the label
  drawLabel(data, label) {
    this.ctx.fillStyle = '# 000'; // Labels default to black

    if(label? .show && label? .formatter) {// If formatter exists
      data.forEach((e, i) = > {
        const x = this.startX + this.diffX * i,
          y = this.baseY - (e - this.Min) * this.diffY;

        this.ctx.fillText(label.formatter.replace(/\{d\}/i, e), x, y - 10);
      });
    } else if(label? .show) {// Formmater does not exist
      data.forEach((e, i) = > {
        const x = this.startX + this.diffX * i,
          y = this.baseY - (e - this.Min) * this.diffY;

        this.ctx.fillText(e, x, y - 10); }); }}// Draw small dots on the fold line
  drawDots(data) {
    this.ctx.beginPath();

    data.forEach((e, i) = > {
      const x = this.startX + this.diffX * i,
        y = this.baseY - (e - this.Min) * this.diffY;

      this.ctx.moveTo(x, y);
      this.ctx.arc(x, y, 3.0.2 * Math.PI);
    });
    this.ctx.fillStyle = this.ctx.strokeStyle;
    this.ctx.fill();
  }

  // Draw a fold line background
  drawBackground(path, el) {
    const { lineStyle } = el;

    if (typeoflineStyle? .backgroundColor ! = ='undefined') {
      const { data } = el;
      const path_ = new Path2D(path);

      path_.lineTo(this.startX + (data.length - 1) * this.diffX, this.baseY); // Baseline endpoint
      path_.lineTo(this.startX, this.baseY); // Baseline starting point

      const lingrad = this.ctx.createLinearGradient(0.0.0.this.height);
      lingrad.addColorStop(0, lineStyle.backgroundColor);
      lingrad.addColorStop(1.'rgba(255,255,255,0)');
      this.ctx.fillStyle = lingrad;

      this.ctx.fill(path_); }}/** * Calculates the bezier curve control point of the current point *@param {Point} PreviousPoint: previousPoint *@param {Point} CurrentPoint: indicates the currentPoint *@param {Point} NextPoint1: The next point *@param {Point} NextPoint2: next point *@param {Number} The coefficient of scale: * /
  calcBezierControlPoints(
    previousPoint,
    currentPoint,
    nextPoint1,
    nextPoint2,
    scale = 0.25
  ) {
    let x = currentPoint.x + scale * (nextPoint1.x - previousPoint.x);
    let y = currentPoint.y + scale * (nextPoint1.y - previousPoint.y);

    const controlPointA = new Point(x, y); // Control point A

    x = nextPoint1.x - scale * (nextPoint2.x - currentPoint.x);
    y = nextPoint1.y - scale * (nextPoint2.y - currentPoint.y);

    const controlPointB = new Point(x, y);

    return { controlPointA, controlPointB };
  }

  /** * Create bezier curve path *@param {*} ctx
   * @param {*} data
   * @param {*} options* /
  createBezierLine(el) {
    const { data } = el;
    const path = new Path2D();
    const { startX, baseY, Min, diffY, diffX } = this;

    path.moveTo(this.startX, this.baseY - (data[0] - this.Min) * this.diffY);

    data.forEach((e, i) = > {
      let curPoint, prePoint, nextPoint1, nextPoint2, x, y;

      / / the current point
      x = startX + diffX * i;
      y = baseY - (e - Min) * diffY;
      curPoint = new Point(x, y);

      // The previous point
      x = startX + diffX * (i - 1);
      y = baseY - (data[i - 1] - Min) * diffY;
      prePoint = new Point(x, y);

      // Next point
      x = startX + diffX * (i + 1);
      y = baseY - (data[i + 1] - Min) * diffY;
      nextPoint1 = new Point(x, y);

      // The next point
      x = startX + diffX * (i + 2);
      y = baseY - (data[i + 2] - Min) * diffY;
      nextPoint2 = new Point(x, y);

      if (i === 0) {
        // If it is the first point, the previous point is replaced by the current point
        prePoint = curPoint;
      } else if (i === data.length - 2) {
        // If it is the penultimate point, the next point is replaced by the next point
        nextPoint2 = nextPoint1;
      } else if (i === data.length - 1) {
        // The last point exits directly
        return;
      }

      const { controlPointA, controlPointB } = this.calcBezierControlPoints(
        prePoint,
        curPoint,
        nextPoint1,
        nextPoint2
      );

      path.bezierCurveTo(
        controlPointA.x,
        controlPointA.y,
        controlPointB.x,
        controlPointB.y,
        nextPoint1.x,
        nextPoint1.y
      );
    });

    return path;
  }

  // Create a polyline path
  createBrokenLine(el) {
    const { data } = el;
    const path = new Path2D();
    data.forEach((e, i) = > {
      const x = this.startX + this.diffX * i,
        y = this.baseY - (e - this.Min) * this.diffY;

      path.lineTo(x, y);
    });

    returnpath; }}Copy the code