{"spec_id":"ecg-twelve-lead","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\n// Standard clinical 12-lead ECG rendered on medical \"paper\": a 3x4 grid of the\n// limb / augmented / precordial leads plus a full-length Lead II rhythm strip.\n// The chart's own gridlines ARE the ECG paper (1 mm minor / 5 mm major, the\n// classic red ruling); the green trace echoes the patient-monitor convention\n// while keeping the Imprint brand green (palette[0]) as the first series.\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE || { width: 1600, height: 900 };\nconst theme = window.ANYPLOT_THEME === \"dark\" ? \"dark\" : \"light\";\n\nconst TRACE = t.palette[0]; // #009E73 — brand green, monitor-style ECG trace\nconst INK = t.ink;\n\n// ECG paper styling — pale pink printout (light) / warm monitor black (dark),\n// red ruling derived from the Imprint matte-red anchor (#AE3030).\nconst PAPER = theme === \"light\" ? \"#FBEFEC\" : \"#201A1A\";\nconst GRID_MAJOR = theme === \"light\" ? \"rgba(174,48,48,0.50)\" : \"rgba(196,72,72,0.42)\";\nconst GRID_MINOR = theme === \"light\" ? \"rgba(174,48,48,0.20)\" : \"rgba(196,72,72,0.16)\";\n\n// --- Geometry (millimetres; standard scale 25 mm/s, 10 mm/mV) ----------------\n// We keep the grid squares truly square by matching the data unit ratio to the\n// plot-area pixel ratio (Highcharts otherwise stretches each axis independently).\nconst MARGIN = { top: 96, bottom: 26, left: 26, right: 26 };\nconst COL_W = 62.5; // 2.5 s per column at 25 mm/s\nconst LEFT_MARGIN = 12; // room for the 1 mV calibration pulse\nconst W_MM = LEFT_MARGIN + 4 * COL_W; // 262 mm total width\nconst plotW = size.width - MARGIN.left - MARGIN.right;\nconst plotH = size.height - MARGIN.top - MARGIN.bottom;\nconst H_MM = (W_MM * plotH) / plotW; // height that keeps 1 mm square\nconst BAND_H = H_MM / 4; // 3 lead rows + 1 rhythm strip\n\nconst colX = (j) => LEFT_MARGIN + j * COL_W;\nconst baseY = (r) => H_MM - (r + 0.5) * BAND_H; // row 0 at top\n\n// Standard clinical layout: columns (I,II,III) (aVR,aVL,aVF) (V1,V2,V3) (V4,V5,V6)\n// arranged so each grid ROW reads I,aVR,V1,V4 / II,aVL,V2,V5 / III,aVF,V3,V6.\nconst ROWS = [\n  [\"I\", \"aVR\", \"V1\", \"V4\"],\n  [\"II\", \"aVL\", \"V2\", \"V5\"],\n  [\"III\", \"aVF\", \"V3\", \"V6\"],\n];\n\n// Per-lead amplitude/polarity gains for a normal-axis sinus rhythm (aVR inverts;\n// the precordial leads show R-wave progression V1->V4->V6).\nconst GAIN = {\n  I: 0.9, II: 1.15, III: 0.55,\n  aVR: -0.95, aVL: 0.45, aVF: 0.7,\n  V1: 0.55, V2: 0.85, V3: 1.05, V4: 1.2, V5: 1.0, V6: 0.8,\n};\n\n// --- Synthetic normal sinus rhythm (deterministic) ---------------------------\nconst T0 = 0.84; // beat period ≈ 71 bpm\nconst DT = 0.002; // 500 Hz render sampling\n\nfunction gauss(tau, a, c, w) {\n  const z = (tau - c) / w;\n  return a * Math.exp(-z * z);\n}\n// One P-QRS-T complex, tau in seconds relative to the R peak.\nfunction beat(tau) {\n  return (\n    gauss(tau, 0.12, -0.20, 0.028) + // P wave\n    gauss(tau, -0.08, -0.038, 0.013) + // Q\n    gauss(tau, 1.10, 0.0, 0.013) + // R\n    gauss(tau, -0.22, 0.040, 0.016) + // S\n    gauss(tau, 0.32, 0.300, 0.070) // T wave\n  );\n}\nfunction signal(tt) {\n  const k0 = Math.floor(tt / T0) - 1;\n  let v = 0;\n  for (let k = k0; k <= k0 + 2; k++) v += beat(tt - k * T0);\n  return v;\n}\n// Tiny fixed-seed LCG for faint, realistic baseline noise.\nlet _seed = 123456789;\nfunction rnd() {\n  _seed = (1103515245 * _seed + 12345) % 2147483648;\n  return _seed / 2147483648;\n}\n// Build a lead trace: time -> (x mm, y mm). Leads are recorded simultaneously,\n// so every column shares the same beat timing (t starts at 0 in each cell).\nfunction leadData(gain, tEnd, xOff, base) {\n  const d = [];\n  for (let tt = 0; tt <= tEnd + 1e-9; tt += DT) {\n    const v = gain * signal(tt) + (rnd() - 0.5) * 0.012;\n    d.push([xOff + tt * 25, base + v * 10]);\n  }\n  return d;\n}\n\n// --- Series ------------------------------------------------------------------\nconst traceOpts = {\n  type: \"line\",\n  color: TRACE,\n  lineWidth: 1.2,\n  marker: { enabled: false },\n  states: { hover: { lineWidthPlus: 0 }, inactive: { opacity: 1 } },\n  enableMouseTracking: true,\n  animation: false,\n};\n\nconst series = [];\n\n// 12 small leads (first series = Lead I, brand green).\nfor (let r = 0; r < 3; r++) {\n  for (let j = 0; j < 4; j++) {\n    const lead = ROWS[r][j];\n    series.push({\n      ...traceOpts,\n      name: lead,\n      data: leadData(GAIN[lead], 2.5, colX(j), baseY(r)),\n    });\n  }\n}\n\n// Full-length Lead II rhythm strip across the bottom band (10 s).\nseries.push({\n  ...traceOpts,\n  name: \"II (rhythm strip)\",\n  data: leadData(GAIN.II, 10, LEFT_MARGIN, baseY(3)),\n});\n\n// 1 mV calibration pulses (10 mm tall, ~0.2 s wide) at the left of every band.\nconst calData = [];\nfor (let r = 0; r < 4; r++) {\n  const b = baseY(r);\n  calData.push([2, b], [3, b], [3, b + 10], [7, b + 10], [7, b], [9, b], [null, null]);\n}\nseries.push({\n  type: \"line\",\n  name: \"1 mV calibration\",\n  data: calData,\n  color: INK,\n  lineWidth: 1.4,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  animation: false,\n  showInLegend: false,\n});\n\n// Lead labels (top-left of each cell) via an invisible scatter + dataLabels.\nconst labelPts = [];\nfor (let r = 0; r < 3; r++)\n  for (let j = 0; j < 4; j++)\n    labelPts.push({ x: colX(j) + 1.5, y: baseY(r) + BAND_H * 0.34, name: ROWS[r][j] });\nlabelPts.push({ x: LEFT_MARGIN + 1.5, y: baseY(3) + BAND_H * 0.34, name: \"II\" });\n\nseries.push({\n  type: \"scatter\",\n  name: \"labels\",\n  data: labelPts,\n  enableMouseTracking: false,\n  showInLegend: false,\n  animation: false,\n  marker: { enabled: false },\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    align: \"left\",\n    verticalAlign: \"middle\",\n    style: { color: INK, fontSize: \"15px\", fontWeight: \"700\", textOutline: \"none\" },\n  },\n});\n\n// --- Chart -------------------------------------------------------------------\nconst axisCommon = {\n  min: 0,\n  startOnTick: false,\n  endOnTick: false,\n  minPadding: 0,\n  maxPadding: 0,\n  lineWidth: 0,\n  tickWidth: 0,\n  tickLength: 0,\n  minorTickLength: 0,\n  tickInterval: 5, // 5 mm bold ruling\n  minorTickInterval: 1, // 1 mm fine ruling\n  gridLineWidth: 1.2,\n  gridLineColor: GRID_MAJOR,\n  minorGridLineWidth: 0.6,\n  minorGridLineColor: GRID_MINOR,\n  labels: { enabled: false },\n  title: { text: null },\n};\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    plotBackgroundColor: PAPER,\n    plotBorderWidth: 1,\n    plotBorderColor: GRID_MAJOR,\n    marginTop: MARGIN.top,\n    marginBottom: MARGIN.bottom,\n    marginLeft: MARGIN.left,\n    marginRight: MARGIN.right,\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"ecg-twelve-lead · javascript · highcharts · anyplot.ai\",\n    style: { color: INK, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Normal sinus rhythm · 25 mm/s · 10 mm/mV · 1 mV calibration\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  xAxis: { ...axisCommon, max: W_MM },\n  yAxis: { ...axisCommon, max: H_MM },\n  plotOptions: { series: { animation: false } },\n  series,\n});\n\nwindow.__anyplotReady = true;\n"}