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:
- 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;
- Add the grid background that we erased earlier (of course, you can also write two Canvas backgrounds to keep it still);
- 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 itdrawLine
The 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