Components / Clinical
ECG trace
A rhythm strip for a recorded trace or a live stream, drawn on standard ECG paper with its P, QRS and T waves labelled. Not for diagnostic use.
Live monitor
Not for diagnostic use.
Recorded strip
Not for diagnostic use.
Original synthetic sinus rhythm generated in the browser. No patient data or clinical validation.
Installation
bunx shadcn@4.22.1 add https://organui.com/r/ecg-trace.json
Or by name: bunx shadcn@4.22.1 add @organui/ecg-trace.
The source installs into your configured UI directory, with shadcn's button for the live pause control. It draws with SVG and your theme tokens, and installs no chart or 3D packages.
Usage
import { EcgTrace } from "@/components/ui/ecg-trace"
// Millivolts, sampled at 500 Hz.
<EcgTrace samples={leadII} sampleRate={500} lead="II" />Pass samples in millivolts and their sampleRate in hertz (250 by default). The strip shows the whole recording unless you set duration in seconds. One lead per component; stack several for a multi-lead view.
Live streams
"use client"
import { useMemo } from "react"
import { EcgTrace, type EcgStream } from "@/components/ui/ecg-trace"
export function Monitor({ socket }: { socket: WebSocket }) {
const stream = useMemo<EcgStream>(() => ({
subscribe(listener) {
const read = (event: MessageEvent) => listener(JSON.parse(event.data))
socket.addEventListener("message", read)
return () => socket.removeEventListener("message", read)
},
}), [socket])
return <EcgTrace stream={stream} sampleRate={250} duration={6} />
}A stream is any object whose subscribe takes a listener for chunks of samples and returns an unsubscribe function. The strip sweeps like a bedside monitor: it overwrites the window in place, with a short blank bar ahead of the write head, and shows 6 seconds unless you set duration. Memoize the stream so the sweep is not reset on every render. The Pause button freezes the sweep and drops samples until it resumes.
Paper, labels and rate
The grid follows standard ECG paper: 1 mm small boxes and 5 mm large boxes, at 25 mm/s and 10 mm/mV by default. Change speed and gain to match your device, and range for the millivolts shown (-1.5 to 2 by default). A 1 mV calibration pulse comes first unless calibration={false}. On narrow screens the strip scrolls sideways rather than shrinking below 3 pixels per millimetre.
The grid is red, like printed ECG paper. To match your theme, set --ecg-grid to any colour in your CSS; the small and large boxes draw it at 15% and 35% opacity.
Labels and the heart rate come from detectEcgWaves, a simple slope and amplitude heuristic tuned for clean sinus rhythms. For real recordings pass your own annotations, each a wave (P, QRS or T) and its peak time in seconds, or annotations={false} to hide them. The heart rate is the median of the R to R intervals.
Synthetic rhythm
import {
EcgTrace, syntheticEcg, syntheticEcgStream,
} from "@/components/ui/ecg-trace"
<EcgTrace samples={syntheticEcg({ heartRate: 60, duration: 10 })} />
<EcgTrace stream={syntheticEcgStream({ heartRate: 90 })} />syntheticEcg and syntheticEcgStream generate an original, regular sinus rhythm from Gaussian P, Q, R, S and T waves, for demos and tests. It is not a recording and models no arrhythmia.
Accessibility and limits
The strip is an image whose name gives the lead, duration, paper settings and heart rate. Wave labels are hidden from assistive technology, which gets the summary instead. The scrolling strip is a focusable region, and a live strip always offers Pause.
An educational display, not a medical device: it does not filter, diagnose or alarm, and its wave detection is not validated. Do not use it for diagnosis or clinical decisions.