I build little health dashboards for clinics. I also teach kids to code on weekends. Funny mix, right? So I had to draw a live ECG line on a web page. Not a fake one. A real, moving line that keeps up with the heart. Smooth, fast, and clear.
If you’d like the full play-by-play of that initial experiment, you can read this deep-dive on building an ECG chart with JavaScript.
My Setup (real, messy, and honest)
- Laptop: MacBook Air M2, 16 GB RAM
- Browser: Chrome 127
- Also tested on a cheap Windows mini PC and a Raspberry Pi 4
- Data: 1-lead at 360 Hz and 500 Hz; then 12-lead at 500 Hz
- Window size: 10 seconds on screen (so 5,000 points per lead at 500 Hz)
I also spilled coffee once and had to restart Chrome. That counts as “real life,” right?
What I Tested
- Smoothie Charts (good for live streams)
- uPlot (very fast, tiny, plain)
- Chart.js (nice and friendly, but can get slow)
- ECharts (feature rich; looks great)
- Plotly (shiny; great for analysis and zoom)
- D3 (roll-your-own; very flexible)
- Raw Canvas (DIY; fastest if you keep it lean)
- LightningChart JS (very fast; paid tier for big stuff)
- JSCharting (robust commercial library; I didn’t benchmark it for ECG yet, but many teams swear by its real-time performance)
By the way, if you need a charting library purpose-built for ECG and other biosignals, take a look at EJsChart — it’s specialized for medical waveforms and trims away a lot of the plumbing.
I’ve used each of these in real projects. Some twice. Some only once. Some I still use every week.
I also published a broader rundown of the pros and cons when I tried a bunch of JavaScript chart libraries and noted what actually worked.
What I Needed the ECG Chart to Do
- Keep 60 FPS when streaming 500 samples per second
- Show 10 seconds of data with no jitters
- Let me zoom and scroll when I pause
- Handle 12 leads without my laptop begging for mercy
- Look like a proper ECG: clean line, no jagged edges, steady baseline
Seems fair, right?
The Quick Winners
1) Smoothie Charts — Easiest Live Feed
I used Smoothie Charts to get a “it works!” demo in 10 minutes. It streams well and feels made for this.
What I liked:
- Dead simple live line
- Good on low power devices
- Very little code
What bugged me:
- Hard to show 12 leads without extra work
- Not the best for zoom, pan, or fancy labels
Real code I used for a 1-lead feed at 500 Hz:
<canvas id="ecg" width="900" height="200"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/smoothie/1.36.0/smoothie.min.js"></script>
<script>
const canvas = document.getElementById('ecg');
const chart = new SmoothieChart({
millisPerPixel: 2, // ~10s on 1000px
grid: { strokeStyle: '#ddd', verticalSections: 10 },
interpolation: 'linear',
maxValue: 2.0, minValue: -2.0
});
chart.streamTo(canvas, /* delay */ 0);
const line = new TimeSeries();
chart.addTimeSeries(line, { strokeStyle: 'rgba(200,0,0,1)', lineWidth: 2 });
// fake sample source: 500 Hz
setInterval(() => {
const now = Date.now();
const sample = Math.sin(now / 15) * 0.4 + (Math.random() - 0.5) * 0.02;
line.append(now, sample);
}, 2);
</script>
On my MacBook, this held 60 FPS for 1 lead. On Raspberry Pi 4, it was still smooth.
2) uPlot — Best Raw Speed for Many Points
uPlot is plain Canvas and very fast. It uses low memory. It doesn’t hand-hold you. That’s fine by me.
What I liked:
- Super fast for 10-second windows
- Great for 12 leads if you draw them in small strips
- Small library; loads quick
What bugged me:
- You wire up streaming yourself
- Zoom is there, but you’ll tweak it
Real code from my clinic viewer (1 lead stream):
<link rel="stylesheet" href="https://unpkg.com/uplot/dist/uPlot.min.css">
<div id="ecg"></div>
<script src="https://unpkg.com/uplot/dist/uPlot.iife.min.js"></script>
<script>
const N = 5000; // 10s * 500Hz
const t = new Array(N);
const y = new Array(N).fill(0);
const start = Date.now();
for (let i = 0; i < N; i++) t[i] = i;
const u = new uPlot({
width: 900,
height: 220,
series: [
{},
{ label: "Lead I", stroke: "red", width: 2 }
],
axes: [
{ scale: "x", values: (u, ticks) => ticks.map(v => (v/500).toFixed(1) + "s") },
{ scale: "y", space: 40, values: (u, ticks) => ticks.map(v => v.toFixed(1) + " mV") }
],
scales: { x: { time: false }, y: { auto: true } }
}, [t, y], document.getElementById('ecg'));
let idx = 0;
function pushSample(sample) {
y[idx % N] = sample;
idx++;
// rotate window logically without copying
const off = idx % N;
const tView = t.map((_, i) => i);
const yView = y.slice(off).concat(y.slice(0, off));
u.setData([tView, yView]);
}
function tick() {
// fake 500 Hz
for (let i = 0; i < 8; i++) { // batch a bit
const now = Date.now() - start;
const s = Math.sin(now / 15) * 0.4 + (Math.random() - 0.5) * 0.02;
pushSample(s);
}
requestAnimationFrame(tick);
}
tick();
</script>
Note: For 12 leads, I made 12 small charts stacked, all sharing time. Still smooth.
That sprint was part of a head-to-head where I pitted seven different JavaScript chart libraries against each other to see which could really keep up.
The “Nice but Careful” Picks
Chart.js
Chart.js looks friendly and feels safe. For slow data it’s great. But at 500 Hz with long windows, it lagged for me.
- 1 lead at 500 Hz: OK for short windows
- 12 leads: it got choppy; redraws were heavy
Plotly
I love Plotly for zoom and explore time. For live ECG? It was heavy after a few minutes with many points. Nice for playback, though.
ECharts
Pretty, rich, feature packed. For streaming ECG, it felt heavier than uPlot and Smoothie. Still fine for 1–3 leads.
D3 (custom)
I built one with D3 Paths. It worked but took more time. I had to tune it a lot. I wouldn’t start here unless I need full control.
If you’re curious about that rabbit hole, here’s the write-up on what actually worked (and didn’t) when I built charts with D3.js.
LightningChart JS
This one flew. WebGL helps. It handled 12 leads like a champ on my laptop. But if you need full features, you’ll hit the paid tier. Worth it for bigger teams or long-term use.
Raw Canvas: Fast, But You Become The Library
When I needed rock-solid speed for 12 leads, I wrote a tiny Canvas renderer. It’s not pretty, but it runs fast and steady.
What I liked:
- Fast even on weak machines
- I control every pixel
- Easy to keep garbage collection low
What I dealt with:
- I had to manage buffers and time axes
- More code to maintain
If you’re
