{"spec_id":"ecg-twelve-lead","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\necg-twelve-lead: ECG/EKG 12-Lead Waveform Display\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\n# Theme-adaptive chrome (Imprint palette)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nLIGHT = THEME == \"light\"\n\nPAGE_BG = \"#FAF8F1\" if LIGHT else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if LIGHT else \"#242420\"\nINK = \"#1A1A17\" if LIGHT else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if LIGHT else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if LIGHT else \"#A8A79F\"\n\n# ECG paper styling — printed paper on light, bedside monitor on dark.\n# Both keep the medical-standard red/salmon grid the spec requires.\nif LIGHT:\n    PAPER_FILL = \"#FFF5F0\"  # warm pinkish ECG recording paper\n    GRID_MINOR = \"rgba(214, 150, 138, 0.32)\"  # 1 mm light lines\n    GRID_MAJOR = \"rgba(196, 108, 96, 0.52)\"  # 5 mm bold lines\n    ZERO_LINE = \"rgba(190, 100, 90, 0.45)\"\nelse:\n    PAPER_FILL = \"#1A1A17\"  # dark cardiac-monitor surface\n    GRID_MINOR = \"rgba(204, 120, 110, 0.18)\"\n    GRID_MAJOR = \"rgba(214, 122, 110, 0.34)\"\n    ZERO_LINE = \"rgba(214, 122, 110, 0.32)\"\n\n# Imprint palette position 1 — the brand green doubles as the classic\n# green cardiac-monitor trace, so the single data series stays on-brand.\nTRACE = \"#009E73\"\n\n# Data — synthetic ECG via a simplified Gaussian P-QRS-T model\nnp.random.seed(42)\n\nsampling_rate = 1000\nduration = 2.5\nt = np.linspace(0, duration, int(sampling_rate * duration))\n\n# Base Lead II signal built inline (KISS — no helper functions)\nbeat_interval = 0.8\nlead_II_signal = np.zeros_like(t)\nfor beat_start in np.arange(0, duration, beat_interval):\n    t_shifted = t - beat_start\n    mask = (t_shifted >= 0) & (t_shifted < beat_interval)\n    tb = t_shifted[mask]\n    lead_II_signal[mask] += (\n        0.15 * np.exp(-((tb - 0.12) ** 2) / (2 * 0.035**2))  # P wave\n        + (-0.12) * np.exp(-((tb - 0.20) ** 2) / (2 * 0.012**2))  # Q wave\n        + 1.2 * np.exp(-((tb - 0.23) ** 2) / (2 * 0.012**2))  # R wave\n        + (-0.25) * np.exp(-((tb - 0.26) ** 2) / (2 * 0.012**2))  # S wave\n        + 0.3 * np.exp(-((tb - 0.38) ** 2) / (2 * 0.045**2))  # T wave\n    )\n\nlead_II_signal += np.random.normal(0, 0.005, len(t))\n\n# Per-lead transforms deriving all 12 leads from Lead II.\n# r_ratio < 0 → precordial leads with deeper S-waves (V1-V2 deepest).\nlead_transforms = {\n    \"I\": {\"scale\": 0.65, \"t_inv\": False},\n    \"II\": {\"scale\": 1.0, \"t_inv\": False},\n    \"III\": {\"scale\": 0.45, \"t_inv\": False},\n    \"aVR\": {\"scale\": 0.75, \"t_inv\": True},\n    \"aVL\": {\"scale\": 0.35, \"t_inv\": False},\n    \"aVF\": {\"scale\": 0.70, \"t_inv\": False},\n    \"V1\": {\"scale\": 0.55, \"r_ratio\": -1.0},\n    \"V2\": {\"scale\": 0.80, \"r_ratio\": -0.7},\n    \"V3\": {\"scale\": 0.95, \"r_ratio\": 0.3},\n    \"V4\": {\"scale\": 1.10, \"r_ratio\": 0.7},\n    \"V5\": {\"scale\": 0.90, \"r_ratio\": 0.9},\n    \"V6\": {\"scale\": 0.70, \"r_ratio\": 1.0},\n}\n\nleads = {}\nfor name, params in lead_transforms.items():\n    signal = lead_II_signal * params[\"scale\"]\n    if params.get(\"t_inv\"):\n        signal = -signal\n    if \"r_ratio\" in params:\n        r_ratio = params[\"r_ratio\"]\n        for beat_start in np.arange(0, duration, beat_interval):\n            t_shifted = t - beat_start\n            mask = (t_shifted >= 0.19) & (t_shifted < 0.28)\n            if r_ratio < 0:\n                # Small r, dominant deep S (rS morphology of V1-V2)\n                r_component = 0.6 * np.exp(-((t_shifted - 0.22) ** 2) / (2 * 0.012**2)) * params[\"scale\"]\n                s_extra = -1.5 * np.exp(-((t_shifted - 0.25) ** 2) / (2 * 0.016**2)) * params[\"scale\"]\n                signal[mask] += r_component[mask] * abs(r_ratio)\n                signal[mask] += s_extra[mask] * abs(r_ratio)\n    leads[name] = signal\n\n# Standard clinical 3x4 column order + Lead II rhythm strip\ngrid_layout = [[\"I\", \"aVR\", \"V1\", \"V4\"], [\"II\", \"aVL\", \"V2\", \"V5\"], [\"III\", \"aVF\", \"V3\", \"V6\"]]\n\n# Plot\nfig = make_subplots(\n    rows=4,\n    cols=4,\n    specs=[[{}, {}, {}, {}], [{}, {}, {}, {}], [{}, {}, {}, {}], [{\"colspan\": 4}, None, None, None]],\n    row_heights=[0.23, 0.23, 0.23, 0.31],\n    vertical_spacing=0.055,\n    horizontal_spacing=0.035,\n    subplot_titles=[\n        \"I\",\n        \"aVR\",\n        \"V1\",\n        \"V4\",\n        \"II\",\n        \"aVL\",\n        \"V2\",\n        \"V5\",\n        \"III\",\n        \"aVF\",\n        \"V3\",\n        \"V6\",\n        \"Lead II — Rhythm Strip\",\n    ],\n)\n\n# ECG signal traces with interactive hover detail\nfor row_idx, row_leads in enumerate(grid_layout):\n    for col_idx, lead_name in enumerate(row_leads):\n        fig.add_trace(\n            go.Scatter(\n                x=t,\n                y=leads[lead_name],\n                mode=\"lines\",\n                line={\"color\": TRACE, \"width\": 1.6},\n                showlegend=False,\n                name=lead_name,\n                hovertemplate=f\"<b>{lead_name}</b><br>Time: %{{x:.3f}} s<br>Voltage: %{{y:.2f}} mV<extra></extra>\",\n            ),\n            row=row_idx + 1,\n            col=col_idx + 1,\n        )\n\n# Full-length Lead II rhythm strip\nfig.add_trace(\n    go.Scatter(\n        x=t,\n        y=leads[\"II\"],\n        mode=\"lines\",\n        line={\"color\": TRACE, \"width\": 1.9},\n        showlegend=False,\n        name=\"Lead II\",\n        hovertemplate=\"<b>Lead II</b><br>Time: %{x:.3f} s<br>Voltage: %{y:.2f} mV<extra></extra>\",\n    ),\n    row=4,\n    col=1,\n)\n\n# 1 mV / 0.2 s calibration pulse at the left margin of every panel\ncal_t = np.array([0.0, 0.0, 0.02, 0.02, 0.04, 0.04]) - 0.085\ncal_v = np.array([0.0, 1.0, 1.0, 0.0, 0.0, 0.0])\n\nfor row_idx in range(4):\n    cols = [1, 2, 3, 4] if row_idx < 3 else [1]\n    for col_idx in cols:\n        fig.add_trace(\n            go.Scatter(\n                x=cal_t,\n                y=cal_v,\n                mode=\"lines\",\n                line={\"color\": INK_SOFT, \"width\": 1.3},\n                showlegend=False,\n                hoverinfo=\"skip\",\n            ),\n            row=row_idx + 1,\n            col=col_idx,\n        )\n\n# Style — medical ECG grid on every axis\nfor row_idx in range(1, 5):\n    cols = [1, 2, 3, 4] if row_idx <= 3 else [1]\n    for col_idx in cols:\n        fig.update_xaxes(\n            range=[-0.12, duration],\n            dtick=0.2,\n            minor={\"dtick\": 0.04, \"gridcolor\": GRID_MINOR, \"gridwidth\": 1, \"showgrid\": True},\n            gridcolor=GRID_MAJOR,\n            gridwidth=1.2,\n            showgrid=True,\n            zeroline=False,\n            showticklabels=(row_idx == 4),\n            tickfont={\"size\": 9, \"color\": INK_SOFT},\n            ticks=\"\",\n            row=row_idx,\n            col=col_idx,\n        )\n        fig.update_yaxes(\n            range=[-1.6, 1.8],\n            dtick=0.5,\n            minor={\"dtick\": 0.1, \"gridcolor\": GRID_MINOR, \"gridwidth\": 1, \"showgrid\": True},\n            gridcolor=GRID_MAJOR,\n            gridwidth=1.2,\n            showgrid=True,\n            zeroline=True,\n            zerolinecolor=ZERO_LINE,\n            zerolinewidth=1,\n            showticklabels=False,\n            ticks=\"\",\n            row=row_idx,\n            col=col_idx,\n        )\n\n# Voltage / time axis labels on the reference panels\nfig.update_yaxes(\n    showticklabels=True,\n    tickfont={\"size\": 9, \"color\": INK_SOFT},\n    title_text=\"mV\",\n    title_font={\"size\": 12, \"color\": INK},\n    row=1,\n    col=1,\n)\nfig.update_yaxes(\n    showticklabels=True,\n    tickfont={\"size\": 9, \"color\": INK_SOFT},\n    title_text=\"mV\",\n    title_font={\"size\": 12, \"color\": INK},\n    row=4,\n    col=1,\n)\nfig.update_xaxes(\n    title_text=\"Time (s)\", title_font={\"size\": 12, \"color\": INK}, tickfont={\"size\": 9, \"color\": INK_SOFT}, row=4, col=1\n)\n\nfig.update_layout(\n    title={\n        \"text\": \"ecg-twelve-lead · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 17, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.985,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAPER_FILL,\n    font={\"color\": INK},\n    showlegend=False,\n    margin={\"l\": 56, \"r\": 24, \"t\": 68, \"b\": 40},\n    hoverlabel={\"bgcolor\": ELEVATED_BG, \"font_size\": 13, \"font_color\": INK, \"bordercolor\": INK_SOFT},\n    hovermode=\"closest\",\n)\n\n# Lead-name subplot titles — bold and legible for clinical identification\nfig.update_annotations(font={\"size\": 14, \"color\": INK, \"family\": \"Arial Black\"})\n\n# Clinical context strip below the title\nfig.add_annotation(\n    text=\"<b>HR 75 bpm</b>  ·  Normal Sinus Rhythm  ·  25 mm/s, 10 mm/mV\",\n    xref=\"paper\",\n    yref=\"paper\",\n    x=0.5,\n    y=1.045,\n    showarrow=False,\n    font={\"size\": 11, \"color\": INK_MUTED, \"family\": \"Arial\"},\n    xanchor=\"center\",\n    yanchor=\"bottom\",\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}