h5ECG: A Canvas Rendering Library That Draws Real ECG Paper, Not Just Curves
Many frontend projects can "draw a curve," but to render real ECG samples into a readable, zoomable, correctly proportioned ECG paper, the details go far beyond a single canvas.lineTo().
I've organized this process into h5ECG: a framework-agnostic ECG Canvas rendering library, providing native, React, and Vue entry points. It encapsulates the grid, waveform, zooming, dragging, 12-lead layout, and measurement ruler; you only need to prepare your own sample data.
This article discusses data visualization and frontend implementation, not for medical diagnosis. Actual clinical use requires compliant data processing, device calibration, and professional review.
The easiest pitfall first: what exactly are your "values"?
Different acquisition devices output different data. Some give mV directly, some give ADC integers, and some include an offset. h5ECG defaults to understanding input based on the calibration in this repository's examples:
- Baseline adcBaseline: 2048
- Conversion factor adcUnitsPerMv: 1000, meaning 1000 ADC units = 1mV
Therefore, a string like 2025, 2025, 2038 is not directly used as pixel height, but is first converted to mV and then drawn according to the gain. If your device parameters differ, you must override these two options; if the ratio is wrong, the waveform may look normal, but the time and voltage meanings are already incorrect.
Installation: npm, GitHub both work
Prefer npm:
npm install h5ecg
If you want to depend directly on the source repository, you can also pin to a released version:
npm install github:dongweiq/h5ecg#v0.1.1
Pinning a tag instead of a branch name makes team builds more stable and results more reproducible.
Minimal working example: draw your own waveform on a page
Prepare a container with a height in the page:
<div id="viewer" style="height: 520px"></div>
Then pass in your own single-lead data:
import { EcgCanvas } from 'h5ecg';
const samples = '2025\n2025\n2038\n2040';
const viewer = new EcgCanvas(document.getElementById('viewer')!, {
mode: '12x1',
speed: 25,
gain: 10,
pixelsPerMillimeter: 8,
adcBaseline: 2048,
adcUnitsPerMv: 1000,
});
viewer.setData(samples);
Three parameters here truly determine the "ECG paper feel":
- speed: 25: 25 mm corresponds to 1 second, so a 1 mm small grid is 40 ms.
- gain: 10: 10 mm corresponds to 1 mV.
- pixelsPerMillimeter: only determines screen display density, does not change the above medical ratios.
To view a more detailed local area, you can directly call:
viewer.zoomBy(1.2);
viewer.setOptions({ speed: 50, gain: 20 });
viewer.resetViewport();
Users can also drag with the mouse, scroll wheel, or pinch-to-zoom. The grid and waveform are drawn in layers, and only visible samples are processed; high-density data retains the peaks and valleys of each pixel bucket when zoomed out, rather than "smoothing away" spikes.
Your data is 12-lead? Pass an array or object directly
h5ECG supports layouts like 12x1, 6x2-sync, 6x2-continuous, 3x4-sync, and 3x4-continuous. You can pass an array of 12 leads, or an object with lead names:
viewer.setOptions({ mode: '3x4-continuous' });
viewer.setData({
I: leadI,
II: leadII,
III: leadIII,
aVR: leadAVR,
aVL: leadAVL,
aVF: leadAVF,
V1: leadV1,
V2: leadV2,
V3: leadV3,
V4: leadV4,
V5: leadV5,
V6: leadV6,
});
This saves much more effort than manually slicing 12 Canvases in business code and calculating the start and end coordinates for each lead.
React and Vue: wrapper components ready to use
React:
import { EcgCanvas } from 'h5ecg/react';
export function ReportView({ samples }: { samples: number[] }) {
return (
<EcgCanvas
data={samples}
mode="6x2-sync"
speed={25}
gain={10}
style={{ height: 520 }}
/>
);
}
Vue:
<script setup lang="ts">
import { EcgCanvas } from 'h5ecg/vue';
const samples = '2025\n2025\n2038';
</script>
<template>
<EcgCanvas
:data="samples"
:options="{ mode: '3x4-continuous', speed: 25, gain: 10 }"
style="height: 520px"
/>
</template>
Add a measurement ruler to turn "viewing" into interactive inspection
Beyond display, the component also supports a draggable measurement ruler that returns duration, voltage, and heart rate estimates:
const viewer = new EcgCanvas(container, {
onRulerChange(measurement) {
console.log(measurement.durationMs);
console.log(measurement.voltageMv);
console.log(measurement.heartRateBpm);
},
});
viewer.setRulerVisible(true);
These results are based on the premise that speed, gain, device calibration, and zoom ratio are correct. Precisely because of this, figuring out the data units first is more important than adjusting colors and line widths.
Give it a try, and contributions are welcome
The online demo is here: h5ECG Demo.
Project source code, Issues, and contribution entry points are here: github.com/dongweiq/h5ecg.
If this library saves you from writing a bunch of Canvas coordinate calculations, a Star is welcome; if you are also working on medical visualization, waveform rendering, or frontend component libraries, a Follow is welcome. I will continue to distill performance, interaction, and data adaptation problems encountered in real projects into this repository.
Top 1 of 3 from juejin.cn, machine-translated. The original thread is authoritative.
A question: for data like EEG, you often need real-time observation, meaning continuous rendering of the latest data (say, sampling every 10ms). With charts needing frequent updates (and the data volume still being large, e.g., 5000 samples), would that cause lag? And if a page renders multiple charts simultaneously, each with the same large data volume and update frequency, can it hold up?
worker
But workers can only handle computation; they can't solve the rendering problem. My main concern here is still the frequent rendering of large data volumes.