{"spec_id":"ecg-twelve-lead","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst TRACE = t.palette[0]; // #009E73 Imprint brand green — the single signal series\n\n// Theme-adaptive ECG \"paper\" grid (light red / pink), drawn over the warm\n// page background instead of pure white so it stays on the Imprint surface.\nconst isDark = window.ANYPLOT_THEME === \"dark\";\nconst GRID_MINOR = isDark ? \"rgba(199,90,90,0.16)\" : \"rgba(174,48,48,0.13)\";\nconst GRID_MAJOR = isDark ? \"rgba(199,90,90,0.34)\" : \"rgba(174,48,48,0.32)\";\n\n// --- Synthetic 12-lead signal (normal sinus rhythm, ~75 bpm) ----------------\n// Each lead is a sum of Gaussian P-Q-R-S-T deflections (mV); per-lead amplitudes\n// encode the normal cardiac axis (aVR inverted, V1 rS → V6 qR progression).\nconst LEADS = {\n  I:   { p: 0.12, q: -0.04, r: 0.75, s: -0.10, t: 0.26 },\n  II:  { p: 0.17, q: -0.04, r: 1.15, s: -0.12, t: 0.34 },\n  III: { p: 0.08, q: -0.03, r: 0.55, s: -0.14, t: 0.16 },\n  aVR: { p: -0.10, q: 0.03, r: -0.55, s: 0.05, t: -0.20 },\n  aVL: { p: 0.07, q: -0.03, r: 0.42, s: -0.08, t: 0.14 },\n  aVF: { p: 0.12, q: -0.03, r: 0.62, s: -0.10, t: 0.22 },\n  V1:  { p: 0.09, q: 0.0, r: 0.28, s: -0.95, t: -0.12 },\n  V2:  { p: 0.11, q: 0.0, r: 0.45, s: -1.25, t: 0.32 },\n  V3:  { p: 0.12, q: -0.02, r: 0.80, s: -0.85, t: 0.42 },\n  V4:  { p: 0.13, q: -0.04, r: 1.35, s: -0.50, t: 0.48 },\n  V5:  { p: 0.13, q: -0.05, r: 1.25, s: -0.25, t: 0.44 },\n  V6:  { p: 0.12, q: -0.05, r: 0.95, s: -0.12, t: 0.34 },\n};\n\n// Deterministic tiny baseline noise (fixed-seed LCG — no Math.random in harness).\nlet seed = 12345;\nconst noise = () => {\n  seed = (1103515245 * seed + 12345) % 2147483648;\n  return (seed / 2147483648 - 0.5) * 0.012;\n};\n\nconst BEAT = 0.8; // seconds per cardiac cycle → 75 bpm\nconst gauss = (ph, mu, sig) => Math.exp(-((ph - mu) * (ph - mu)) / (2 * sig * sig));\nfunction voltage(cfg, time) {\n  const ph = time % BEAT;\n  return (\n    cfg.p * gauss(ph, 0.12, 0.022) +\n    cfg.q * gauss(ph, 0.232, 0.0085) +\n    cfg.r * gauss(ph, 0.252, 0.011) +\n    cfg.s * gauss(ph, 0.272, 0.011) +\n    cfg.t * gauss(ph, 0.40, 0.040) +\n    noise()\n  );\n}\n\n// --- Layout geometry (millimetre paper space; 25 mm/s, 10 mm/mV) ------------\n// Standard clinical 3×4 grid + a full-length Lead II rhythm strip beneath it.\nconst GRID = [\n  [\"I\", \"aVR\", \"V1\", \"V4\"],\n  [\"II\", \"aVL\", \"V2\", \"V5\"],\n  [\"III\", \"aVF\", \"V3\", \"V6\"],\n];\nconst LEFT = 10;        // left margin (holds the calibration pulse)\nconst STRIP_MM = 62.5;  // 2.5 s column at 25 mm/s\nconst MM_PER_S = 25;\nconst MM_PER_MV = 10;\nconst ROW_Y = [122, 88, 54]; // baselines, top → bottom\nconst RHYTHM_Y = 20;\nconst X_MAX = LEFT + 4 * STRIP_MM + 5; // 265\nconst Y_MAX = 136.2; // tuned so 1 mm paper squares stay square at 16:9\nconst DT = 0.002;\n\n// One cell of the grid: a 2.5 s window of the continuous recording.\nfunction cellTrace(lead, col, baseline) {\n  const cfg = LEADS[lead];\n  const t0 = 2.5 * col;\n  const pts = [];\n  for (let time = t0; time <= t0 + 2.5 + 1e-9; time += DT) {\n    pts.push({ x: LEFT + col * STRIP_MM + (time - t0) * MM_PER_S, y: baseline + voltage(cfg, time) * MM_PER_MV });\n  }\n  return pts;\n}\n\nconst datasets = [];\nGRID.forEach((row, r) =>\n  row.forEach((lead, c) => datasets.push({ data: cellTrace(lead, c, ROW_Y[r]) }))\n);\n\n// Full 10 s Lead II rhythm strip across the bottom.\nconst rhythm = [];\nfor (let time = 0; time <= 10 + 1e-9; time += DT) {\n  rhythm.push({ x: LEFT + time * MM_PER_S, y: RHYTHM_Y + voltage(LEADS.II, time) * MM_PER_MV });\n}\ndatasets.push({ data: rhythm });\n\n// 1 mV calibration pulse at the left margin of every row.\nconst calPulse = (b) => [\n  { x: 1, y: b }, { x: 3.5, y: b }, { x: 3.5, y: b + 10 },\n  { x: 8.5, y: b + 10 }, { x: 8.5, y: b }, { x: 9.5, y: b },\n];\n[...ROW_Y, RHYTHM_Y].forEach((b) => datasets.push({ data: calPulse(b) }));\n\n// Shared trace styling — thin crisp green pen, no markers.\ndatasets.forEach((d) => {\n  d.borderColor = TRACE;\n  d.borderWidth = 1.3;\n  d.pointRadius = 0;\n  d.tension = 0;\n  d.fill = false;\n  d.borderJoinStyle = \"round\";\n});\n\n// --- ECG paper, lead labels, title (custom plugins) -------------------------\nconst ecgPaper = {\n  id: \"ecgPaper\",\n  beforeDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const xs = chart.scales.x;\n    const ys = chart.scales.y;\n    const px = (v) => xs.getPixelForValue(v);\n    const py = (v) => ys.getPixelForValue(v);\n\n    const drawLines = (step, width, color) => {\n      ctx.save();\n      ctx.lineWidth = width;\n      ctx.strokeStyle = color;\n      ctx.beginPath();\n      for (let x = 0; x <= X_MAX + 1e-6; x += step) {\n        ctx.moveTo(px(x), py(0));\n        ctx.lineTo(px(x), py(Y_MAX));\n      }\n      for (let y = 0; y <= Y_MAX + 1e-6; y += step) {\n        ctx.moveTo(px(0), py(y));\n        ctx.lineTo(px(X_MAX), py(y));\n      }\n      ctx.stroke();\n      ctx.restore();\n    };\n    drawLines(1, 1, GRID_MINOR);\n    drawLines(5, 1.6, GRID_MAJOR);\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea: a } = chart;\n    const xs = chart.scales.x;\n    const ys = chart.scales.y;\n    const px = (v) => xs.getPixelForValue(v);\n    const py = (v) => ys.getPixelForValue(v);\n\n    // Lead labels, anchored top-left of each cell.\n    ctx.save();\n    ctx.fillStyle = t.ink;\n    ctx.font = \"bold 17px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"alphabetic\";\n    GRID.forEach((row, r) =>\n      row.forEach((lead, c) => ctx.fillText(lead, px(LEFT + c * STRIP_MM + 2), py(ROW_Y[r] + 15.5)))\n    );\n    ctx.fillText(\"II  ·  rhythm strip\", px(LEFT + 2), py(RHYTHM_Y + 15.5));\n    ctx.restore();\n\n    // Title in the reserved top band.\n    ctx.save();\n    ctx.fillStyle = t.ink;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = \"bold 22px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n    ctx.fillText(\"ecg-twelve-lead · javascript · chartjs · anyplot.ai\", (a.left + a.right) / 2, a.top / 2);\n    ctx.restore();\n\n    // Scale footnote in the reserved bottom band.\n    ctx.save();\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"right\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = \"13px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n    ctx.fillText(\n      \"synthetic normal sinus rhythm, 75 bpm  ·  25 mm/s  ·  10 mm/mV  ·  1 mV calibration pulse\",\n      a.right,\n      a.bottom + (chart.height - a.bottom) / 2\n    );\n    ctx.restore();\n  },\n};\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    parsing: false,\n    layout: { padding: { left: 10, right: 10, top: 58, bottom: 30 } },\n    plugins: {\n      legend: { display: false },\n      title: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: { type: \"linear\", min: 0, max: X_MAX, display: false },\n      y: { type: \"linear\", min: 0, max: Y_MAX, display: false },\n    },\n  },\n  plugins: [ecgPaper],\n});\n"}