ECG/EKG 12-Lead Waveform Display — MUI X Charts

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 MUI X Charts

JavaScript source (MUI X Charts)

// anyplot.ai
// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display
// Library: muix 7.29.1 | JavaScript 22.22.3
// Quality: 93/100 | Created: 2026-06-17
//# anyplot-orientation: landscape
// anyplot.ai
// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display
// Library: MUI X Charts | React | Node 22
// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.
// Quality: pending | Created: 2026-06-17
import { LineChart } from "@mui/x-charts/LineChart";

const t = window.ANYPLOT_TOKENS;
const isLight = window.ANYPLOT_THEME !== "dark";

// Imprint palette: the waveform trace is brand green (position 1, always first
// series). ECG paper grid uses matte red (Imprint position 5) — the universally
// recognised colour of clinical ECG paper — at theme-adaptive alpha.
const TRACE = t.palette[0]; // #009E73 brand green
// Both themes derive from the same Imprint matte-red anchor (#AE3030 = rgba(174,48,48));
// the dark theme simply raises the alpha so the grid stays visible on the near-black
// surface, rather than introducing an off-palette brightened red.
const GRID_MINOR = isLight ? "rgba(174,48,48,0.16)" : "rgba(174,48,48,0.40)";
const GRID_MAJOR = isLight ? "rgba(174,48,48,0.34)" : "rgba(174,48,48,0.66)";

// --- Synthetic ECG model (in-memory, deterministic) ------------------------
// Normal sinus rhythm at 75 bpm. Each P-QRS-T complex is a sum of Gaussian
// deflections; per-lead profiles scale P/R/S/T amplitude and polarity to give
// each of the 12 leads its characteristic morphology (e.g. aVR fully inverted,
// V1 rS with inverted T, V4-V6 dominant R waves).
const FS = 120; // chart sampling (Hz) — smooth without bloating the SVG paths
const PERIOD = 0.8; // 75 bpm
const CAL_DUR = 0.34; // 1 mV calibration pulse occupies the strip margin

function ecgValue(tb, p) {
  const g = (c, a, w) => a * Math.exp(-((tb - c) ** 2) / (2 * w * w));
  return (
    g(0.18, 0.13 * p.p, 0.022) + // P wave
    g(0.355, -0.09, 0.011) + // Q
    g(0.39, 1.0 * p.r, 0.011) + // R
    g(0.42, -0.28 * p.s, 0.013) + // S
    g(0.585, 0.3 * p.t, 0.042) // T wave
  );
}

function buildStrip(p, seconds) {
  const xs = [];
  const ys = [];
  const total = CAL_DUR + seconds;
  const n = Math.round(total * FS);
  for (let i = 0; i <= n; i++) {
    const time = i / FS;
    xs.push(time);
    if (time < CAL_DUR) {
      // square 1 mV calibration pulse
      ys.push(time >= 0.07 && time < 0.24 ? 1.0 : 0.0);
    } else {
      const tb = (time - CAL_DUR) % PERIOD;
      ys.push(p.flip * ecgValue(tb, p));
    }
  }
  return { xs, ys };
}

// Per-lead morphology profiles (p=P-gain, r=R-gain, s=S-gain, t=T-gain, flip=polarity)
const PROFILE = {
  I: { p: 1.0, r: 0.75, s: 0.25, t: 0.9, flip: 1 },
  II: { p: 1.2, r: 1.05, s: 0.3, t: 1.1, flip: 1 },
  III: { p: 0.7, r: 0.55, s: 0.35, t: 0.55, flip: 1 },
  aVR: { p: 1.0, r: 0.6, s: 0.2, t: 0.9, flip: -1 },
  aVL: { p: 0.6, r: 0.5, s: 0.3, t: 0.5, flip: 1 },
  aVF: { p: 0.9, r: 0.8, s: 0.25, t: 0.8, flip: 1 },
  V1: { p: 0.7, r: 0.3, s: 1.0, t: -0.5, flip: 1 },
  V2: { p: 0.8, r: 0.55, s: 1.35, t: 0.5, flip: 1 },
  V3: { p: 0.8, r: 0.95, s: 0.95, t: 0.85, flip: 1 },
  V4: { p: 0.85, r: 1.45, s: 0.5, t: 1.0, flip: 1 },
  V5: { p: 0.9, r: 1.4, s: 0.25, t: 1.0, flip: 1 },
  V6: { p: 0.9, r: 1.1, s: 0.15, t: 0.9, flip: 1 },
};

// Standard clinical 3x4 layout (read left-to-right per row)
const GRID_LEADS = [
  ["I", "aVR", "V1", "V4"],
  ["II", "aVL", "V2", "V5"],
  ["III", "aVF", "V3", "V6"],
];

// --- Layout geometry (CSS px in the mount space) ---------------------------
const W = window.ANYPLOT_SIZE.width;
const H = window.ANYPLOT_SIZE.height;
const PAD = 16;
const TITLE_H = 44;
const GAP = 8;
const RHYTHM_H = 150;
const innerW = W - 2 * PAD;
const gridH = H - 2 * PAD - TITLE_H - GAP - RHYTHM_H - GAP;
const cellW = (innerW - 3 * GAP) / 4;
const cellH = (gridH - 2 * GAP) / 3;

// Common voltage window so every lead shares the standard 10 mm/mV scale
const Y_MIN = -1.7;
const Y_MAX = 1.9;

const STRIP_SECONDS = 2.5;
const RHYTHM_SECONDS = 6.0;

function ecgPaper() {
  const SM = 8; // 1 mm minor square
  const BIG = SM * 5; // 5 mm major square
  return {
    backgroundColor: t.pageBg,
    backgroundImage: [
      `repeating-linear-gradient(0deg, ${GRID_MAJOR}, ${GRID_MAJOR} 1px, transparent 1px, transparent ${BIG}px)`,
      `repeating-linear-gradient(90deg, ${GRID_MAJOR}, ${GRID_MAJOR} 1px, transparent 1px, transparent ${BIG}px)`,
      `repeating-linear-gradient(0deg, ${GRID_MINOR}, ${GRID_MINOR} 1px, transparent 1px, transparent ${SM}px)`,
      `repeating-linear-gradient(90deg, ${GRID_MINOR}, ${GRID_MINOR} 1px, transparent 1px, transparent ${SM}px)`,
    ].join(", "),
  };
}

function LeadTrace(props) {
  const { strip, width, height } = props;
  return (
    <LineChart
      width={width}
      height={height}
      margin={{ top: 24, right: 8, bottom: 8, left: 8 }}
      skipAnimation
      leftAxis={null}
      bottomAxis={null}
      xAxis={[{ data: strip.xs, scaleType: "linear", min: 0, max: strip.xs[strip.xs.length - 1] }]}
      yAxis={[{ min: Y_MIN, max: Y_MAX }]}
      series={[{ data: strip.ys, color: TRACE, showMark: false, curve: "linear" }]}
      sx={{
        "& .MuiLineElement-root": { strokeWidth: 2.0, strokeLinejoin: "round" },
      }}
    />
  );
}

function LeadPanel(props) {
  const { name, width, height } = props;
  const strip = buildStrip(PROFILE[name], STRIP_SECONDS);
  return (
    <div style={{ position: "relative", width, height, ...ecgPaper() }}>
      <span
        style={{
          position: "absolute",
          top: 4,
          left: 8,
          zIndex: 2,
          fontSize: 15,
          fontWeight: 700,
          color: t.ink,
          background: isLight ? "rgba(250,248,241,0.7)" : "rgba(26,26,23,0.7)",
          padding: "0 4px",
          borderRadius: 2,
        }}
      >
        {name}
      </span>
      <LeadTrace strip={strip} width={width} height={height} />
    </div>
  );
}

// --- Chart (default-exported component — the harness mounts it) -------------
export default function Chart() {
  const rhythm = buildStrip(PROFILE.II, RHYTHM_SECONDS);
  return (
    <div
      style={{
        width: W,
        height: H,
        boxSizing: "border-box",
        padding: PAD,
        display: "flex",
        flexDirection: "column",
        gap: GAP,
        background: t.pageBg,
        fontFamily:
          '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
      }}
    >
      {/* Header */}
      <div
        style={{
          height: TITLE_H,
          display: "flex",
          alignItems: "baseline",
          justifyContent: "space-between",
        }}
      >
        <span style={{ fontSize: 22, fontWeight: 700, color: t.ink }}>
          ecg-twelve-lead · javascript · muix · anyplot.ai
        </span>
        <span style={{ fontSize: 17, color: t.inkSoft }}>
          Normal sinus rhythm · 25 mm/s · 10 mm/mV · 1 mV calibration
        </span>
      </div>

      {/* 3x4 lead grid */}
      <div
        style={{
          display: "grid",
          gridTemplateColumns: `repeat(4, ${cellW}px)`,
          gridTemplateRows: `repeat(3, ${cellH}px)`,
          gap: GAP,
        }}
      >
        {GRID_LEADS.flat().map((name) => (
          <LeadPanel key={name} name={name} width={cellW} height={cellH} />
        ))}
      </div>

      {/* Full-length Lead II rhythm strip */}
      <div style={{ position: "relative", width: innerW, height: RHYTHM_H, ...ecgPaper() }}>
        <span
          style={{
            position: "absolute",
            top: 4,
            left: 8,
            zIndex: 2,
            fontSize: 15,
            fontWeight: 700,
            color: t.ink,
            background: isLight ? "rgba(250,248,241,0.7)" : "rgba(26,26,23,0.7)",
            padding: "0 4px",
            borderRadius: 2,
          }}
        >
          II · rhythm
        </span>
        <LineChart
          width={innerW}
          height={RHYTHM_H}
          margin={{ top: 24, right: 8, bottom: 8, left: 8 }}
          skipAnimation
          leftAxis={null}
          bottomAxis={null}
          xAxis={[{ data: rhythm.xs, scaleType: "linear", min: 0, max: rhythm.xs[rhythm.xs.length - 1] }]}
          yAxis={[{ min: Y_MIN, max: Y_MAX }]}
          series={[{ data: rhythm.ys, color: TRACE, showMark: false, curve: "linear" }]}
          sx={{ "& .MuiLineElement-root": { strokeWidth: 2.0, strokeLinejoin: "round" } }}
        />
      </div>
    </div>
  );
}

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

Other implementations