Recently, we received a new demand, which requires the front-end to receive the data from the box to draw the ELECTROcardiogram dynamically with canvas. Here is the most basic single-lead electrocardiogram drawing method. We used the React framework, and the final results are as follows:

HTML tags

Since the width and height of our canvas can be customized, I wrote variables as follows:

<canvas id="ecg" width={this.state.width} height={this.state.height}></canvas>
Copy the code

Draw the background

As you can see from the picture, we can think of the background as two types of tables, the small table every 5 pixels and the large table every 25 pixels, so that it is easier to understand, step by step, draw the small table first:

// I split the two methods because I need to draw the table twice
drawGrid = () = >{
    let myCanvas = document.getElementById('ecg');
    If canvas is not supported, no error will be reported
    if(myCanvas.getContext){
      let ctx = myCanvas.getContext('2d');
      let { width, height} = this.state;
      this.drawSmallGrid(ctx,width,height);
      this.drawBigGrid(ctx,width,height); }}// Draw a small table background
drawSmallGrid = (ctx,width,height) = >{
    // Set the line color
    ctx.strokeStyle = '#f1dedf';
    // Line thickness
    ctx.lineWidth = 1;
    ctx.beginPath();
    / / draw a vertical bar
    // Start with 0 and the width is the width of the canvas, 5 pixels at a time
    for(let x = 0; x <= width; x += 5) {// Where should I start every time I reposition my position
      ctx.moveTo(x,0);
      // Draw the entire canvas height line at a time
      ctx.lineTo(x,height);
      ctx.stroke();
    }
    // Do the same for the horizontal line
    for(let y = 0; y <= height; y += 5){
      ctx.moveTo(0,y);
      ctx.lineTo(width,y);
      ctx.stroke();
    }
    ctx.closePath();
  }
Copy the code

So you get something like this

So let’s draw a big table background

// Large tables and small tables work the same way
drawBigGrid = (ctx,width,height) = >{
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.strokeStyle = '# 663333';
    // The only difference is the color and spacing
    for(let x = 0; x <= width; x += 25){
      ctx.moveTo(x,0);
      ctx.lineTo(x,height);
      ctx.stroke();
    }
    for(let y = 0; y <= height; y += 25){
      ctx.moveTo(0,y);
      ctx.lineTo(width,y);
      ctx.stroke();
    }
    ctx.closePath();
  }
Copy the code

In fact, these two can be drawn together, but I just didn’t optimize the code to distinguish them, so after drawing the big table it looks like this:

However, the effect is strange, it is clearly a line with the width of one pixel, but it looks like two pixels. I read the explanation on the Internet: The drawing method of canvas lines is different, each line of canvas has an infinitely thin “middle line”, and the width of the line extends from the middle line to both sides. If we or by drawing a line from the pixels, then the center line of line will do to the starting point of the pixels, and then we began to draw, the problem would be to: on both sides of the line of Canvas in the midline to extension, rather than to the side (such as if only the unilateral extension, so our problem is no longer a problem) at this time and have a question: Computers don’t allow graphics smaller than 1px, so he did a compromise: he drew both pixels. The line in Canvas aligns the center line with the starting point of the pixel, not the middle point of the pixel. So, in this case, a 1px line looks like a 2px line. You can also find on the Internet how to draw 1 pixel, but here you just use one of the center translation methods, you just align the center of the line with the center of the pixel. Modify the drawGrid method as follows:

drawGrid = () = >{
    let myCanvas = document.getElementById('ecg');
    if(myCanvas.getContext){
      let ctx = myCanvas.getContext('2d');
      let { width, height} = this.state;
      The translate() method remaps the position (0,0) on the canvas. Let's move it by 0.5 so it's aligned
      ctx.translate(0.5.0.5)
      this.drawSmallGrid(ctx,width,height);
      this.drawBigGrid(ctx,width,height); }}Copy the code

The effect is thisThis is much more comfortable to look at, take special care to make sure that all your coordinate points are integers, otherwise HTML5 will automatically implement edge anti-aliasing, causing your 1 pixel line to look thick again.

If the following situation occurs, it means that the width and height are too high and should be larger because we have remapped the position (0,0), so the width and height are increasing by one pixel

Electrocardiogram drawing

Next is the key, how to draw a line, because it is a dynamic ecg drawing, certainly need a timer, so we should think about each step of what to draw, sum up a few points:

  1. First of all, I’m going to clear the canvas that I drew before (I’m only going to clear a part of it, not all of it), and I’m going to clear the point that I’m going to draw a few pixels ahead of the x axis, so that people can see at a glance that I’m here;
  2. Add the grid background that we erased earlier (of course, you can also write two Canvas backgrounds to keep it still);
  3. Finally just draw electrocardiogram, the order is certain cannot be disorderly, otherwise covered, what present finally is certain heart wave.

Once we understand the steps, we can start writing

state = {
    width: 751.height: 126.// The processed data
    beatArray: [].// Select the data we should display according to the speed of the paper
    sampling: 2.// The X-axis coordinates
    pointX: 0.// The initial y coordinate
    startPointY: 0.// Where is the y-coordinate drawn
    endPointY: 0.// Get the data source
    dataSource: '- 12-13-14-17-17-15-12-12-13-14-12-12-10-10-12-13-15-17-16-16-14-13-14-16-18-16-17-18-17-16 -15 -14 -14 -13 -12 -15 -15 -17 -20 -21 -18 -16 -16 -11 -9 -10 -10 -12 -13 -16 -14 -17 -16 -15 -15 -14 -13 -15 -12 -10 -14 -14 -14 -12 -13 -15 -16 -17 -16 -13 -14 -14 -15 -17 -18 -17 -15 - 12-13-15-17-16-17-16-13-17-17-15-18-16-17-20-17-18 to 19-15-13-14-16-17-16-16-15-14-16-15 to 16 -19 -18 -17 -15 -19 -20 -17 -18 -17 -16 -17 -14 -10 -15 -11 -12 -13 -13 -11 -10 -9 -9 -8 - 10-11-13-9-10-10-5-5-7-6-5-4-4-4 1, 1-3-6 10-14-14-14-12-15-17-18-20-22-21-19-18-20 -20 -19 -22 -23 -24 -22 -23 -27 -26 -24 -23 -24 -21 -18 -19 -21 -24 -24 -23 -23 -23 -21-17-15-16-16-11-9-1 10 21 29 40 51 66 85 99 99 99 102 90 71 52 28 9-17-46-60-60-58-55-48-42-38-35 -31 -31 -28 -26 -24 -22 -22 -17 -17 -21 -21 -20 -19 -18 -16 -16 -17 -17 -14 -15 -15 -14 -14 -16 -17 -15 -11 -9 -11 -11 -13 -10 -13 -12 -12 -13 -10 -8 -5 -4 -4 -8 -9 -7 -9 -9 -10 -8 -4 -2 -2 -1 -2 -4-4-5-5-5-4-4-1 0-3-4-3-1 2 1-1 14 6 5 5 6 4 4 6 7 6 9 8 10 11 11 13 14 13 15 15 18 20 23 23 22 22 24 23 23 24 28 34 36 36 36 36 39 38 38 41 43 43 43 44 46 47 51 50 47 49 49 50 50 47 49 53 51 50 48 49 48 43 40 37 33 31 27 23 21 19 18 13 7 6 3 0 15 4-5-6-6-6-7 10-11-12-12-14-12-9 10-11-11-11-11-13-15-18-14-10 -11 -12 -15 -16 -18 -19 -19 -14 -13 -13 -11 -14 -14 -13 -12 -13 -13 -15 -15 -14 -14 -15 -17 -16 -16 -17 -15 -15 -15 -16 -18 -13 -13 -14 -13 -11 -9 -10 -14 -14 -14 -13 -12 -9 -12 -11 -9 -10 -12 -13 -13 -12 -14 -14 -16 -14 -11 -12 -12 -14 -16 -17 -16 -15 -13 -10 -11 -11 -13 -16 -16 -16 -16 -16 -16 -15 -13 -10 -11 -11 -13 -16 -16 -16 -16 -16 -16 -16 -15 -13 -10 -11 -11 -13 -16 -16 -16 -16 -16 -16 -16 -16 -16 -15 -13 -10 -11 -11 -13 -16 -15 -16 -16 -14 -12 -10 -12 -12 -16 -17 -18 -16 -17 -19 -21 -16 -13 -11 -15 -14 -15 -16 -16 -16 -17 -17 17-16-19-20 - '
  }
  // Process the data we need first
  getData = () = > {
    let { dataSource, beatArray, sampling } = this.state;
    // Convert to an array
    var arr = dataSource.split(' ');
    // We selectively process data according to the paper speed
    for (let i = 0; i < arr.length; i += sampling) {
      beatArray.push(arr[i]);
    }
    this.setState({
      beatArray
    }, () = > {
      // Set the timer
      var myCanvas = document.getElementById('ecg');
      if (myCanvas.getContext) {
        var ctx = myCanvas.getContext('2d');
        this.timer = setInterval(() = > {
          this.drawLine(ctx)
          // If there is no data, the timer will stop automatically. If there is no data, the timer will appear as the beginning of the article
          if (this.state.beatArray.length === 0) {
            clearInterval(this.timer)
          }
        }, 100)}}}// Start drawing the line as you thought
  drawLine = (ctx) = > {
    let { width, height, beatArray, pointX, startPointY, endPointY } = this.state;
    // Start with the middle of the canvas
    startPointY = height / 2;
    // If the X-axis covers the entire canvas, start from the beginning
    if (pointX >= width) {
      pointX = -5
    }
    // Delete the last drawing and add a breakpoint background
    ctx.beginPath();
    ctx.strokeStyle = '#FFF'
    ctx.lineWidth = 2;
    ctx.clearRect(pointX + 4.0.25, height - 2)
    ctx.moveTo(pointX + 10.0)
    ctx.lineTo(pointX + 10, height)
    ctx.stroke();
    ctx.closePath();
    // Add background
    // Add the small ones first
    ctx.beginPath();
    ctx.strokeStyle = "#f1dedf";
    ctx.lineWidth = 1;
    for (let y = 0; y < height; y += 5) {
      ctx.moveTo(pointX, y);
      ctx.lineTo(pointX + 5, y);
      ctx.stroke();
    }
    ctx.moveTo(pointX, 0);
    ctx.lineTo(pointX, height);
    ctx.stroke();
    ctx.closePath();
    // Add the big one
    ctx.beginPath();
    ctx.strokeStyle = "# 663333";
    ctx.lineWidth = 1;
    for (let y = 0; y < height; y += 25) {
      ctx.moveTo(pointX, y);
      ctx.lineTo(pointX + 5, y);
      ctx.stroke();
    }
    if (pointX % 25= = =0) {
      ctx.moveTo(pointX, 0);
      ctx.lineTo(pointX, height);
      ctx.stroke();
    }
    ctx.closePath();
    // Heart wave
    ctx.beginPath();
    ctx.strokeStyle = "green";
    ctx.lineWidth = 2;
    // If the x-coordinate is 0, it means to start from the beginning, so that no line is drawn from the end to the beginning
    if (pointX === 0) {
      ctx.moveTo(0, startPointY)
    } else {
      // Otherwise move to the position you drew last time
      ctx.moveTo(pointX, endPointY)
    }
    // Five pixels at a time is one grid
    pointX += 5;
    // Calculate the y position from the middle point of the canvas
    endPointY = startPointY - beatArray[0];
    Draw a line / /
    ctx.lineTo(pointX, endPointY)
    // Draw a point to delete a point from the data
    beatArray.shift()
    ctx.stroke();
    ctx.closePath();
    // Update the result once
    this.setState({
      beatArray,
      pointX,
      endPointY,
    })
  }
  
Copy the code

So we can see the effect:However, I found a problem: the steeper the slope is, how can I feel that there are fewer things? It seems that the two points were not very continuous before, was it covered by something

The original is that we have to reposition a point every time we start a new drawing, first moveTo and then lineTo. If it is always lineTo, this problem will not occur, but we have to reposition it (novice do not know how to do), and then think of a problem, since lineTo will not appear this problem, That every time we draw two lines, each line drawing to repeat the last painting line, so that directly between two lineTo should not have this problem, but every time we finish painting first later to delete all the data before, can’t get the data to do the last picture, finally thought of a way to write a log variables, save the painting line recently, Go ahead and fix itdrawLineThe method is as follows:

  drawLine = (ctx) = > {
    let { width, height, beatArray, startPointY, endPointY, pointX, pointLog } = this.state;
    // Start with the middle of the canvas
    startPointY = height / 2;
    // If the X-axis covers the entire canvas, start from the beginning
    if (pointX >= width) {
      pointX = -5
    }
    // Delete the last drawing and add a breakpoint background
    ctx.beginPath();
    ctx.strokeStyle = '#FFF'
    ctx.lineWidth = 2;
    ctx.clearRect(pointX + 4.0.25, height)
    ctx.moveTo(pointX + 10.0)
    ctx.lineTo(pointX + 10, height)
    ctx.stroke();
    ctx.closePath();
    // Add background
    // Add the small ones first
    ctx.beginPath();
    ctx.strokeStyle = "#f1dedf";
    ctx.lineWidth = 1;
    for (let y = 0; y < height; y += 5) {
      ctx.moveTo(pointX, y);
      ctx.lineTo(pointX + 5, y);
      ctx.stroke();
    }
    ctx.moveTo(pointX, 0);
    ctx.lineTo(pointX, height);
    ctx.stroke();
    ctx.closePath();
    // Add the big one
    ctx.beginPath();
    ctx.strokeStyle = "# 663333";
    ctx.lineWidth = 1;
    for (let y = 0; y < height; y += 25) {
      ctx.moveTo(pointX, y);
      ctx.lineTo(pointX + 5, y);
      ctx.stroke();
    }
    if (pointX % 25= = =0) {
      ctx.moveTo(pointX, 0);
      ctx.lineTo(pointX, height);
      ctx.stroke();
    }
    ctx.closePath();
    // Heart wave
    ctx.beginPath();
    ctx.strokeStyle = "green";
    ctx.lineWidth = 2;
    ctx.lineCap = "square";
    ctx.lineJoin = "round";
    // If the x-coordinate is 0, it means to start from the beginning, so that no line is drawn from the end to the beginning
    if (pointX === 0) {
      ctx.moveTo(0, startPointY)
    } else {
      // Write a drawing log so that he does not have the problem that two lines seem to be disconnected
      if (pointLog.length === 3) {
        ctx.moveTo(pointLog[1].x, pointLog[1].y)
      } else {
        ctx.moveTo(pointX, endPointY)
      }
    }
    // Five pixels at a time is one grid
    pointX += 5;
    // After receiving the data, calculate the position that should be in the Y-axis. If not, directly give the position as the center line.
    endPointY = startPointY - beatArray[0] || startPointY;
    // Repeat the path drawn last time to prevent intermediate breakpoints
    if (pointLog[2] && pointX ! = =0) {
      ctx.lineTo(pointLog[2].x, pointLog[2].y)
    }
    // Write it again to repeat the last drawing to the end, so that the previous definition is in vain
    if (pointX === 0) {
      ctx.moveTo(0, startPointY)
    }
    ctx.lineTo(pointX, endPointY)
    // Save the position of each drawing
    pointLog.push({ 'x': pointX, 'y': endPointY })
    // Save the data for 3 times, delete the excess from the beginning
    if (pointLog.length > 3) {
      pointLog.shift()
    }
    // Draw a point to delete a point from the data
    beatArray.shift()
    ctx.stroke();
    ctx.closePath();
    // Update the result once
    this.setState({
      beatArray,
      pointX,
      endPointY,
      pointLog
    })
  }
Copy the code

Perfect. That’s what we did at the beginning of this article.

The last

New contact, but also in continuous improvement, if there is any wrong criticism, thank you very much