{"spec_id":"ecg-twelve-lead","library":"echarts","language":"javascript","code":"// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: echarts 5.5.1 | JavaScript 22\n// Quality: pending | Created: 2026-06-17\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\nconst W = window.ANYPLOT_SIZE.width;\nconst H = window.ANYPLOT_SIZE.height;\n\n// --- Theme-adaptive chrome --------------------------------------------------\n// The waveform trace is a single signal type repeated across all 12 leads, so it\n// uses the Imprint brand green (palette[0]). The ECG paper grid keeps its\n// real-world domain convention — a light red/pink ruling — derived from the\n// Imprint matte-red anchor, tuned per theme for contrast on cream / near-black.\nconst TRACE = t.palette[0]; // #009E73 — brand green, first (and only) series colour\nconst MAJOR_GRID = THEME === \"light\" ? \"rgba(199,68,68,0.42)\" : \"rgba(228,108,108,0.34)\";\nconst MINOR_GRID = THEME === \"light\" ? \"rgba(199,68,68,0.16)\" : \"rgba(228,108,108,0.12)\";\n\n// --- ECG paper geometry -----------------------------------------------------\n// Standard scale: 25 mm/s horizontal, 10 mm/mV vertical. One big square (5 mm)\n// spans 0.20 s and 0.5 mV; small squares (1 mm) subdivide it 5x. A continuous\n// coordinate system carries all 12 leads + the rhythm strip so the paper ruling\n// is unbroken, exactly like a printed clinical ECG.\nconst RR = 0.8; // 75 bpm — normal sinus rhythm\nconst COL_SEC = 2.5; // seconds shown per lead column (2.5 s @ 25 mm/s)\nconst DT = 0.004; // sampling step for the rendered trace\n\n// Lead morphology — per-lead {P, Q, R, S, T} amplitudes in mV. Q/S are negative.\n// aVR is globally inverted; precordials show R-wave progression (V1 deep rS to\n// V5/V6 tall qR), the textbook normal pattern.\nconst PROFILES = {\n  I: { p: 0.10, q: -0.05, r: 0.70, s: -0.08, t: 0.20 },\n  II: { p: 0.15, q: -0.05, r: 1.20, s: -0.10, t: 0.30 },\n  III: { p: 0.07, q: -0.04, r: 0.55, s: -0.10, t: 0.15 },\n  aVR: { p: -0.10, q: 0.05, r: -0.55, s: 0.04, t: -0.20 },\n  aVL: { p: 0.05, q: -0.05, r: 0.45, s: -0.10, t: 0.12 },\n  aVF: { p: 0.11, q: -0.04, r: 0.80, s: -0.08, t: 0.22 },\n  V1: { p: 0.08, q: 0.0, r: 0.25, s: -0.90, t: -0.10 },\n  V2: { p: 0.10, q: 0.0, r: 0.45, s: -1.30, t: 0.35 },\n  V3: { p: 0.10, q: -0.03, r: 0.80, s: -0.90, t: 0.45 },\n  V4: { p: 0.12, q: -0.05, r: 1.35, s: -0.50, t: 0.50 },\n  V5: { p: 0.12, q: -0.06, r: 1.45, s: -0.25, t: 0.45 },\n  V6: { p: 0.11, q: -0.06, r: 1.20, s: -0.12, t: 0.35 },\n};\n\n// Standard clinical 3x4 arrangement; a full-length Lead II rhythm strip runs\n// across the bottom.\nconst LAYOUT = [\n  [\"I\", \"aVR\", \"V1\", \"V4\"],\n  [\"II\", \"aVL\", \"V2\", \"V5\"],\n  [\"III\", \"aVF\", \"V3\", \"V6\"],\n];\nconst ROW_BASELINE = [9, 6, 3]; // mV offset for each grid row\nconst RHYTHM_BASELINE = 0; // mV offset for the bottom rhythm strip\n\n// One P-QRS-T complex modelled as a sum of Gaussians (tt = seconds into a beat).\nconst gauss = (x, c, w) => Math.exp(-((x - c) * (x - c)) / (2 * w * w));\nfunction ecgBeat(tt, prof) {\n  return (\n    prof.p * gauss(tt, 0.17, 0.022) +\n    prof.q * gauss(tt, 0.330, 0.0085) +\n    prof.r * gauss(tt, 0.365, 0.011) +\n    prof.s * gauss(tt, 0.405, 0.012) +\n    prof.t * gauss(tt, 0.560, 0.045)\n  );\n}\n\n// Sample one lead over a time window. All 12 leads are recorded simultaneously,\n// so the four columns are consecutive slices of the same run — phase is\n// continuous left-to-right (globalTime), which is how printed ECGs read.\nfunction leadTrace(prof, globalStart, durationSec, baseline) {\n  const pts = [];\n  const n = Math.round(durationSec / DT);\n  for (let i = 0; i <= n; i++) {\n    const lt = i * DT;\n    const gt = globalStart + lt;\n    pts.push([gt, baseline + ecgBeat(gt % RR, prof)]);\n  }\n  return pts;\n}\n\n// 1 mV / 0.2 s rectangular calibration pulse drawn in the left margin of a row.\nfunction calPulse(baseline) {\n  return [\n    [-0.55, baseline],\n    [-0.40, baseline],\n    [-0.40, baseline + 1],\n    [-0.20, baseline + 1],\n    [-0.20, baseline],\n    [-0.05, baseline],\n  ];\n}\n\n// --- Build series -----------------------------------------------------------\nconst lineDefaults = {\n  type: \"line\",\n  showSymbol: false,\n  smooth: false,\n  clip: true,\n  silent: true,\n  z: 5,\n  lineStyle: { color: TRACE, width: 1.8, join: \"round\" },\n};\n\nconst series = [];\nLAYOUT.forEach((rowLeads, r) => {\n  rowLeads.forEach((name, c) => {\n    series.push({\n      ...lineDefaults,\n      data: leadTrace(PROFILES[name], c * COL_SEC, COL_SEC, ROW_BASELINE[r]),\n    });\n  });\n  series.push({ ...lineDefaults, data: calPulse(ROW_BASELINE[r]) });\n});\n// Full-length Lead II rhythm strip across the bottom (all four columns wide).\nseries.push({\n  ...lineDefaults,\n  data: leadTrace(PROFILES[\"II\"], 0, COL_SEC * 4, RHYTHM_BASELINE),\n});\nseries.push({ ...lineDefaults, data: calPulse(RHYTHM_BASELINE) });\n\n// --- Coordinate window + pixel mapping for lead labels ----------------------\nconst GL = 64, GR = 36, GT = 122, GB = 75; // grid margins (CSS px)\nconst X0 = -0.6, X1 = 10.0; // seconds (4 columns x 2.5 s, plus cal margin)\nconst Y0 = -1.5, Y1 = 11.0; // mV\nconst gW = W - GL - GR;\nconst gH = H - GT - GB;\nconst px = (x) => GL + ((x - X0) / (X1 - X0)) * gW;\nconst py = (y) => GT + ((Y1 - y) / (Y1 - Y0)) * gH;\n\nconst graphic = [];\nLAYOUT.forEach((rowLeads, r) => {\n  rowLeads.forEach((name, c) => {\n    graphic.push({\n      type: \"text\",\n      left: px(c * COL_SEC + 0.05),\n      top: py(ROW_BASELINE[r] + 1.45),\n      style: { text: name, fill: t.ink, font: \"bold 17px sans-serif\" },\n    });\n  });\n});\ngraphic.push({\n  type: \"text\",\n  left: px(0.05),\n  top: py(RHYTHM_BASELINE + 1.35),\n  style: { text: \"II\", fill: t.ink, font: \"bold 17px sans-serif\" },\n});\n// Standard paper-scale annotation, bottom-left.\ngraphic.push({\n  type: \"text\",\n  left: GL,\n  top: H - 26,\n  style: {\n    text: \"25 mm/s     10 mm/mV     1 mV cal\",\n    fill: t.inkSoft,\n    font: \"13px sans-serif\",\n  },\n});\n\n// --- Axis (ECG paper ruling) ------------------------------------------------\nconst ecgAxis = (min, max, interval) => ({\n  type: \"value\",\n  min,\n  max,\n  interval,\n  axisLabel: { show: false },\n  axisLine: { show: false },\n  axisTick: { show: false },\n  splitLine: { show: true, lineStyle: { color: MAJOR_GRID, width: 1 } },\n  minorTick: { show: false, splitNumber: 5 },\n  minorSplitLine: { show: true, lineStyle: { color: MINOR_GRID, width: 0.6 } },\n});\n\n// --- Init + render ----------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: t.pageBg,\n  title: {\n    text: \"ecg-twelve-lead · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },\n  },\n  grid: { left: GL, right: GR, top: GT, bottom: GB, containLabel: false },\n  xAxis: ecgAxis(X0, X1, 0.2),\n  yAxis: ecgAxis(Y0, Y1, 0.5),\n  graphic,\n  series,\n});\n\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}