{"spec_id":"ecg-twelve-lead","library":"muix","language":"javascript","code":"// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-17\nimport { LineChart } from \"@mui/x-charts/LineChart\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst isLight = window.ANYPLOT_THEME !== \"dark\";\n\n// Imprint palette: the waveform trace is brand green (position 1, always first\n// series). ECG paper grid uses matte red (Imprint position 5) — the universally\n// recognised colour of clinical ECG paper — at theme-adaptive alpha.\nconst TRACE = t.palette[0]; // #009E73 brand green\n// Both themes derive from the same Imprint matte-red anchor (#AE3030 = rgba(174,48,48));\n// the dark theme simply raises the alpha so the grid stays visible on the near-black\n// surface, rather than introducing an off-palette brightened red.\nconst GRID_MINOR = isLight ? \"rgba(174,48,48,0.16)\" : \"rgba(174,48,48,0.40)\";\nconst GRID_MAJOR = isLight ? \"rgba(174,48,48,0.34)\" : \"rgba(174,48,48,0.66)\";\n\n// --- Synthetic ECG model (in-memory, deterministic) ------------------------\n// Normal sinus rhythm at 75 bpm. Each P-QRS-T complex is a sum of Gaussian\n// deflections; per-lead profiles scale P/R/S/T amplitude and polarity to give\n// each of the 12 leads its characteristic morphology (e.g. aVR fully inverted,\n// V1 rS with inverted T, V4-V6 dominant R waves).\nconst FS = 120; // chart sampling (Hz) — smooth without bloating the SVG paths\nconst PERIOD = 0.8; // 75 bpm\nconst CAL_DUR = 0.34; // 1 mV calibration pulse occupies the strip margin\n\nfunction ecgValue(tb, p) {\n  const g = (c, a, w) => a * Math.exp(-((tb - c) ** 2) / (2 * w * w));\n  return (\n    g(0.18, 0.13 * p.p, 0.022) + // P wave\n    g(0.355, -0.09, 0.011) + // Q\n    g(0.39, 1.0 * p.r, 0.011) + // R\n    g(0.42, -0.28 * p.s, 0.013) + // S\n    g(0.585, 0.3 * p.t, 0.042) // T wave\n  );\n}\n\nfunction buildStrip(p, seconds) {\n  const xs = [];\n  const ys = [];\n  const total = CAL_DUR + seconds;\n  const n = Math.round(total * FS);\n  for (let i = 0; i <= n; i++) {\n    const time = i / FS;\n    xs.push(time);\n    if (time < CAL_DUR) {\n      // square 1 mV calibration pulse\n      ys.push(time >= 0.07 && time < 0.24 ? 1.0 : 0.0);\n    } else {\n      const tb = (time - CAL_DUR) % PERIOD;\n      ys.push(p.flip * ecgValue(tb, p));\n    }\n  }\n  return { xs, ys };\n}\n\n// Per-lead morphology profiles (p=P-gain, r=R-gain, s=S-gain, t=T-gain, flip=polarity)\nconst PROFILE = {\n  I: { p: 1.0, r: 0.75, s: 0.25, t: 0.9, flip: 1 },\n  II: { p: 1.2, r: 1.05, s: 0.3, t: 1.1, flip: 1 },\n  III: { p: 0.7, r: 0.55, s: 0.35, t: 0.55, flip: 1 },\n  aVR: { p: 1.0, r: 0.6, s: 0.2, t: 0.9, flip: -1 },\n  aVL: { p: 0.6, r: 0.5, s: 0.3, t: 0.5, flip: 1 },\n  aVF: { p: 0.9, r: 0.8, s: 0.25, t: 0.8, flip: 1 },\n  V1: { p: 0.7, r: 0.3, s: 1.0, t: -0.5, flip: 1 },\n  V2: { p: 0.8, r: 0.55, s: 1.35, t: 0.5, flip: 1 },\n  V3: { p: 0.8, r: 0.95, s: 0.95, t: 0.85, flip: 1 },\n  V4: { p: 0.85, r: 1.45, s: 0.5, t: 1.0, flip: 1 },\n  V5: { p: 0.9, r: 1.4, s: 0.25, t: 1.0, flip: 1 },\n  V6: { p: 0.9, r: 1.1, s: 0.15, t: 0.9, flip: 1 },\n};\n\n// Standard clinical 3x4 layout (read left-to-right per row)\nconst GRID_LEADS = [\n  [\"I\", \"aVR\", \"V1\", \"V4\"],\n  [\"II\", \"aVL\", \"V2\", \"V5\"],\n  [\"III\", \"aVF\", \"V3\", \"V6\"],\n];\n\n// --- Layout geometry (CSS px in the mount space) ---------------------------\nconst W = window.ANYPLOT_SIZE.width;\nconst H = window.ANYPLOT_SIZE.height;\nconst PAD = 16;\nconst TITLE_H = 44;\nconst GAP = 8;\nconst RHYTHM_H = 150;\nconst innerW = W - 2 * PAD;\nconst gridH = H - 2 * PAD - TITLE_H - GAP - RHYTHM_H - GAP;\nconst cellW = (innerW - 3 * GAP) / 4;\nconst cellH = (gridH - 2 * GAP) / 3;\n\n// Common voltage window so every lead shares the standard 10 mm/mV scale\nconst Y_MIN = -1.7;\nconst Y_MAX = 1.9;\n\nconst STRIP_SECONDS = 2.5;\nconst RHYTHM_SECONDS = 6.0;\n\nfunction ecgPaper() {\n  const SM = 8; // 1 mm minor square\n  const BIG = SM * 5; // 5 mm major square\n  return {\n    backgroundColor: t.pageBg,\n    backgroundImage: [\n      `repeating-linear-gradient(0deg, ${GRID_MAJOR}, ${GRID_MAJOR} 1px, transparent 1px, transparent ${BIG}px)`,\n      `repeating-linear-gradient(90deg, ${GRID_MAJOR}, ${GRID_MAJOR} 1px, transparent 1px, transparent ${BIG}px)`,\n      `repeating-linear-gradient(0deg, ${GRID_MINOR}, ${GRID_MINOR} 1px, transparent 1px, transparent ${SM}px)`,\n      `repeating-linear-gradient(90deg, ${GRID_MINOR}, ${GRID_MINOR} 1px, transparent 1px, transparent ${SM}px)`,\n    ].join(\", \"),\n  };\n}\n\nfunction LeadTrace(props) {\n  const { strip, width, height } = props;\n  return (\n    <LineChart\n      width={width}\n      height={height}\n      margin={{ top: 24, right: 8, bottom: 8, left: 8 }}\n      skipAnimation\n      leftAxis={null}\n      bottomAxis={null}\n      xAxis={[{ data: strip.xs, scaleType: \"linear\", min: 0, max: strip.xs[strip.xs.length - 1] }]}\n      yAxis={[{ min: Y_MIN, max: Y_MAX }]}\n      series={[{ data: strip.ys, color: TRACE, showMark: false, curve: \"linear\" }]}\n      sx={{\n        \"& .MuiLineElement-root\": { strokeWidth: 2.0, strokeLinejoin: \"round\" },\n      }}\n    />\n  );\n}\n\nfunction LeadPanel(props) {\n  const { name, width, height } = props;\n  const strip = buildStrip(PROFILE[name], STRIP_SECONDS);\n  return (\n    <div style={{ position: \"relative\", width, height, ...ecgPaper() }}>\n      <span\n        style={{\n          position: \"absolute\",\n          top: 4,\n          left: 8,\n          zIndex: 2,\n          fontSize: 15,\n          fontWeight: 700,\n          color: t.ink,\n          background: isLight ? \"rgba(250,248,241,0.7)\" : \"rgba(26,26,23,0.7)\",\n          padding: \"0 4px\",\n          borderRadius: 2,\n        }}\n      >\n        {name}\n      </span>\n      <LeadTrace strip={strip} width={width} height={height} />\n    </div>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const rhythm = buildStrip(PROFILE.II, RHYTHM_SECONDS);\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        boxSizing: \"border-box\",\n        padding: PAD,\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: GAP,\n        background: t.pageBg,\n        fontFamily:\n          '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif',\n      }}\n    >\n      {/* Header */}\n      <div\n        style={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"baseline\",\n          justifyContent: \"space-between\",\n        }}\n      >\n        <span style={{ fontSize: 22, fontWeight: 700, color: t.ink }}>\n          ecg-twelve-lead · javascript · muix · anyplot.ai\n        </span>\n        <span style={{ fontSize: 17, color: t.inkSoft }}>\n          Normal sinus rhythm · 25 mm/s · 10 mm/mV · 1 mV calibration\n        </span>\n      </div>\n\n      {/* 3x4 lead grid */}\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: `repeat(4, ${cellW}px)`,\n          gridTemplateRows: `repeat(3, ${cellH}px)`,\n          gap: GAP,\n        }}\n      >\n        {GRID_LEADS.flat().map((name) => (\n          <LeadPanel key={name} name={name} width={cellW} height={cellH} />\n        ))}\n      </div>\n\n      {/* Full-length Lead II rhythm strip */}\n      <div style={{ position: \"relative\", width: innerW, height: RHYTHM_H, ...ecgPaper() }}>\n        <span\n          style={{\n            position: \"absolute\",\n            top: 4,\n            left: 8,\n            zIndex: 2,\n            fontSize: 15,\n            fontWeight: 700,\n            color: t.ink,\n            background: isLight ? \"rgba(250,248,241,0.7)\" : \"rgba(26,26,23,0.7)\",\n            padding: \"0 4px\",\n            borderRadius: 2,\n          }}\n        >\n          II · rhythm\n        </span>\n        <LineChart\n          width={innerW}\n          height={RHYTHM_H}\n          margin={{ top: 24, right: 8, bottom: 8, left: 8 }}\n          skipAnimation\n          leftAxis={null}\n          bottomAxis={null}\n          xAxis={[{ data: rhythm.xs, scaleType: \"linear\", min: 0, max: rhythm.xs[rhythm.xs.length - 1] }]}\n          yAxis={[{ min: Y_MIN, max: Y_MAX }]}\n          series={[{ data: rhythm.ys, color: TRACE, showMark: false, curve: \"linear\" }]}\n          sx={{ \"& .MuiLineElement-root\": { strokeWidth: 2.0, strokeLinejoin: \"round\" } }}\n        />\n      </div>\n    </div>\n  );\n}\n"}