ECG/EKG 12-Lead Waveform Display — Highcharts

A multi-channel electrocardiogram display showing the 12 standard ECG leads arranged in clinical format. Each lead displays realistic P-QRS-T wave complexes on a medical-standard grid background with calibration markers. This visualization replicates the familiar layout used in hospitals and clinics worldwide, making it immediately recognizable to healthcare professionals and useful for medical education.

ECG/EKG 12-Lead Waveform Display rendered with Highcharts

JavaScript source (Highcharts)

// anyplot.ai
// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display
// Library: highcharts 12.6.0 | JavaScript 22.22.3
// Quality: 91/100 | Created: 2026-06-17
//# anyplot-orientation: landscape

// Standard clinical 12-lead ECG rendered on medical "paper": a 3x4 grid of the
// limb / augmented / precordial leads plus a full-length Lead II rhythm strip.
// The chart's own gridlines ARE the ECG paper (1 mm minor / 5 mm major, the
// classic red ruling); the green trace echoes the patient-monitor convention
// while keeping the Imprint brand green (palette[0]) as the first series.

const t = window.ANYPLOT_TOKENS;
const size = window.ANYPLOT_SIZE || { width: 1600, height: 900 };
const theme = window.ANYPLOT_THEME === "dark" ? "dark" : "light";

const TRACE = t.palette[0]; // #009E73 — brand green, monitor-style ECG trace
const INK = t.ink;

// ECG paper styling — pale pink printout (light) / warm monitor black (dark),
// red ruling derived from the Imprint matte-red anchor (#AE3030).
const PAPER = theme === "light" ? "#FBEFEC" : "#201A1A";
const GRID_MAJOR = theme === "light" ? "rgba(174,48,48,0.50)" : "rgba(196,72,72,0.42)";
const GRID_MINOR = theme === "light" ? "rgba(174,48,48,0.20)" : "rgba(196,72,72,0.16)";

// --- Geometry (millimetres; standard scale 25 mm/s, 10 mm/mV) ----------------
// We keep the grid squares truly square by matching the data unit ratio to the
// plot-area pixel ratio (Highcharts otherwise stretches each axis independently).
const MARGIN = { top: 96, bottom: 26, left: 26, right: 26 };
const COL_W = 62.5; // 2.5 s per column at 25 mm/s
const LEFT_MARGIN = 12; // room for the 1 mV calibration pulse
const W_MM = LEFT_MARGIN + 4 * COL_W; // 262 mm total width
const plotW = size.width - MARGIN.left - MARGIN.right;
const plotH = size.height - MARGIN.top - MARGIN.bottom;
const H_MM = (W_MM * plotH) / plotW; // height that keeps 1 mm square
const BAND_H = H_MM / 4; // 3 lead rows + 1 rhythm strip

const colX = (j) => LEFT_MARGIN + j * COL_W;
const baseY = (r) => H_MM - (r + 0.5) * BAND_H; // row 0 at top

// Standard clinical layout: columns (I,II,III) (aVR,aVL,aVF) (V1,V2,V3) (V4,V5,V6)
// arranged so each grid ROW reads I,aVR,V1,V4 / II,aVL,V2,V5 / III,aVF,V3,V6.
const ROWS = [
  ["I", "aVR", "V1", "V4"],
  ["II", "aVL", "V2", "V5"],
  ["III", "aVF", "V3", "V6"],
];

// Per-lead amplitude/polarity gains for a normal-axis sinus rhythm (aVR inverts;
// the precordial leads show R-wave progression V1->V4->V6).
const GAIN = {
  I: 0.9, II: 1.15, III: 0.55,
  aVR: -0.95, aVL: 0.45, aVF: 0.7,
  V1: 0.55, V2: 0.85, V3: 1.05, V4: 1.2, V5: 1.0, V6: 0.8,
};

// --- Synthetic normal sinus rhythm (deterministic) ---------------------------
const T0 = 0.84; // beat period ≈ 71 bpm
const DT = 0.002; // 500 Hz render sampling

function gauss(tau, a, c, w) {
  const z = (tau - c) / w;
  return a * Math.exp(-z * z);
}
// One P-QRS-T complex, tau in seconds relative to the R peak.
function beat(tau) {
  return (
    gauss(tau, 0.12, -0.20, 0.028) + // P wave
    gauss(tau, -0.08, -0.038, 0.013) + // Q
    gauss(tau, 1.10, 0.0, 0.013) + // R
    gauss(tau, -0.22, 0.040, 0.016) + // S
    gauss(tau, 0.32, 0.300, 0.070) // T wave
  );
}
function signal(tt) {
  const k0 = Math.floor(tt / T0) - 1;
  let v = 0;
  for (let k = k0; k <= k0 + 2; k++) v += beat(tt - k * T0);
  return v;
}
// Tiny fixed-seed LCG for faint, realistic baseline noise.
let _seed = 123456789;
function rnd() {
  _seed = (1103515245 * _seed + 12345) % 2147483648;
  return _seed / 2147483648;
}
// Build a lead trace: time -> (x mm, y mm). Leads are recorded simultaneously,
// so every column shares the same beat timing (t starts at 0 in each cell).
function leadData(gain, tEnd, xOff, base) {
  const d = [];
  for (let tt = 0; tt <= tEnd + 1e-9; tt += DT) {
    const v = gain * signal(tt) + (rnd() - 0.5) * 0.012;
    d.push([xOff + tt * 25, base + v * 10]);
  }
  return d;
}

// --- Series ------------------------------------------------------------------
const traceOpts = {
  type: "line",
  color: TRACE,
  lineWidth: 1.2,
  marker: { enabled: false },
  states: { hover: { lineWidthPlus: 0 }, inactive: { opacity: 1 } },
  enableMouseTracking: true,
  animation: false,
};

const series = [];

// 12 small leads (first series = Lead I, brand green).
for (let r = 0; r < 3; r++) {
  for (let j = 0; j < 4; j++) {
    const lead = ROWS[r][j];
    series.push({
      ...traceOpts,
      name: lead,
      data: leadData(GAIN[lead], 2.5, colX(j), baseY(r)),
    });
  }
}

// Full-length Lead II rhythm strip across the bottom band (10 s).
series.push({
  ...traceOpts,
  name: "II (rhythm strip)",
  data: leadData(GAIN.II, 10, LEFT_MARGIN, baseY(3)),
});

// 1 mV calibration pulses (10 mm tall, ~0.2 s wide) at the left of every band.
const calData = [];
for (let r = 0; r < 4; r++) {
  const b = baseY(r);
  calData.push([2, b], [3, b], [3, b + 10], [7, b + 10], [7, b], [9, b], [null, null]);
}
series.push({
  type: "line",
  name: "1 mV calibration",
  data: calData,
  color: INK,
  lineWidth: 1.4,
  marker: { enabled: false },
  enableMouseTracking: false,
  animation: false,
  showInLegend: false,
});

// Lead labels (top-left of each cell) via an invisible scatter + dataLabels.
const labelPts = [];
for (let r = 0; r < 3; r++)
  for (let j = 0; j < 4; j++)
    labelPts.push({ x: colX(j) + 1.5, y: baseY(r) + BAND_H * 0.34, name: ROWS[r][j] });
labelPts.push({ x: LEFT_MARGIN + 1.5, y: baseY(3) + BAND_H * 0.34, name: "II" });

series.push({
  type: "scatter",
  name: "labels",
  data: labelPts,
  enableMouseTracking: false,
  showInLegend: false,
  animation: false,
  marker: { enabled: false },
  dataLabels: {
    enabled: true,
    format: "{point.name}",
    align: "left",
    verticalAlign: "middle",
    style: { color: INK, fontSize: "15px", fontWeight: "700", textOutline: "none" },
  },
});

// --- Chart -------------------------------------------------------------------
const axisCommon = {
  min: 0,
  startOnTick: false,
  endOnTick: false,
  minPadding: 0,
  maxPadding: 0,
  lineWidth: 0,
  tickWidth: 0,
  tickLength: 0,
  minorTickLength: 0,
  tickInterval: 5, // 5 mm bold ruling
  minorTickInterval: 1, // 1 mm fine ruling
  gridLineWidth: 1.2,
  gridLineColor: GRID_MAJOR,
  minorGridLineWidth: 0.6,
  minorGridLineColor: GRID_MINOR,
  labels: { enabled: false },
  title: { text: null },
};

Highcharts.chart("container", {
  chart: {
    backgroundColor: "transparent",
    plotBackgroundColor: PAPER,
    plotBorderWidth: 1,
    plotBorderColor: GRID_MAJOR,
    marginTop: MARGIN.top,
    marginBottom: MARGIN.bottom,
    marginLeft: MARGIN.left,
    marginRight: MARGIN.right,
    animation: false,
    style: { fontFamily: "inherit" },
  },
  credits: { enabled: false },
  colors: t.palette,
  title: {
    text: "ecg-twelve-lead · javascript · highcharts · anyplot.ai",
    style: { color: INK, fontSize: "22px", fontWeight: "600" },
  },
  subtitle: {
    text: "Normal sinus rhythm · 25 mm/s · 10 mm/mV · 1 mV calibration",
    style: { color: t.inkSoft, fontSize: "14px" },
  },
  legend: { enabled: false },
  tooltip: { enabled: false },
  xAxis: { ...axisCommon, max: W_MM },
  yAxis: { ...axisCommon, max: H_MM },
  plotOptions: { series: { animation: false } },
  series,
});

window.__anyplotReady = true;

Part of ECG/EKG 12-Lead Waveform Display on anyplot.ai.

Other implementations