{"spec_id":"ecg-twelve-lead","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\necg-twelve-lead: ECG/EKG 12-Lead Waveform Display\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport seaborn.objects as so\n\n\n# Theme-adaptive chrome (Imprint)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette: the ECG trace is the single data series, so it takes the brand\n# green (#009E73) — also the classic green-on-monitor cardiac look. The iconic ECG\n# paper grid uses the matte-red medical/blood semantic anchor (#AE3030).\nBRAND = \"#009E73\"\nGRID_RED = \"#AE3030\"\ngrid_major_alpha = 0.50 if THEME == \"light\" else 0.55\ngrid_minor_alpha = 0.20 if THEME == \"light\" else 0.24\n\nsns.set_theme(\n    style=\"white\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"text.color\": INK,\n        \"axes.labelcolor\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n    },\n)\n\n# Data - synthetic normal sinus rhythm via a Gaussian P-QRS-T wave model, emitted\n# into one long-form (tidy) DataFrame so seaborn can facet the 12 leads natively.\nnp.random.seed(42)\nsampling_rate = 1000\nduration = 2.5\ntime = np.linspace(0, duration, int(sampling_rate * duration))\nheart_rate = 72\nrr_interval = 60.0 / heart_rate\n\nwave_centers = np.array([0.10, 0.22, 0.25, 0.28, 0.42])\nwave_widths = np.array([0.012, 0.005, 0.008, 0.006, 0.025])\nwave_keys = [\"p\", \"q\", \"r\", \"s\", \"t\"]\n\nlead_configs = {\n    \"I\": {\"p\": 0.15, \"q\": -0.08, \"r\": 0.9, \"s\": -0.15, \"t\": 0.25},\n    \"II\": {\"p\": 0.20, \"q\": -0.10, \"r\": 1.2, \"s\": -0.20, \"t\": 0.35},\n    \"III\": {\"p\": 0.08, \"q\": -0.05, \"r\": 0.6, \"s\": -0.10, \"t\": 0.15},\n    \"aVR\": {\"p\": -0.15, \"q\": 0.05, \"r\": -0.5, \"s\": 0.10, \"t\": -0.25},\n    \"aVL\": {\"p\": 0.05, \"q\": -0.06, \"r\": 0.5, \"s\": -0.08, \"t\": 0.12},\n    \"aVF\": {\"p\": 0.12, \"q\": -0.08, \"r\": 0.8, \"s\": -0.15, \"t\": 0.22},\n    \"V1\": {\"p\": 0.10, \"q\": -0.04, \"r\": 0.3, \"s\": -0.8, \"t\": -0.15},\n    \"V2\": {\"p\": 0.12, \"q\": -0.05, \"r\": 0.5, \"s\": -0.6, \"t\": 0.20},\n    \"V3\": {\"p\": 0.12, \"q\": -0.06, \"r\": 0.8, \"s\": -0.4, \"t\": 0.30},\n    \"V4\": {\"p\": 0.14, \"q\": -0.08, \"r\": 1.1, \"s\": -0.25, \"t\": 0.35},\n    \"V5\": {\"p\": 0.14, \"q\": -0.08, \"r\": 1.0, \"s\": -0.18, \"t\": 0.30},\n    \"V6\": {\"p\": 0.12, \"q\": -0.06, \"r\": 0.8, \"s\": -0.12, \"t\": 0.25},\n}\n\n\ndef synth_ecg(t, gains):\n    \"\"\"Sum Gaussian P-Q-R-S-T waves across every beat in the window, add fine noise.\"\"\"\n    signal = np.zeros_like(t)\n    gain_values = np.array([gains[k] for k in wave_keys])\n    for beat_start in np.arange(0, t[-1] + rr_interval, rr_interval):\n        dt = t - beat_start\n        for i in range(5):\n            signal += gain_values[i] * np.exp(-((dt - wave_centers[i]) ** 2) / (2 * wave_widths[i] ** 2))\n    return signal + np.random.normal(0, 0.01, len(t))\n\n\ndef style_ecg_grid(ax, xmax, x_major, x_minor):\n    \"\"\"Render an axis as standard ECG paper: major/minor red grid, no ticks, framed.\"\"\"\n    ax.set_xlim(0, xmax)\n    ax.set_ylim(-1.8, 2.0)\n    ax.set_title(\"\")\n    ax.xaxis.set_major_locator(ticker.MultipleLocator(x_major))\n    ax.xaxis.set_minor_locator(ticker.MultipleLocator(x_minor))\n    ax.yaxis.set_major_locator(ticker.MultipleLocator(0.5))\n    ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.1))\n    ax.grid(which=\"major\", color=GRID_RED, alpha=grid_major_alpha, linewidth=0.5)\n    ax.grid(which=\"minor\", color=GRID_RED, alpha=grid_minor_alpha, linewidth=0.3)\n    ax.set_facecolor(PAGE_BG)\n    ax.set_xticklabels([])\n    ax.set_yticklabels([])\n    ax.set_xlabel(\"\")\n    ax.set_ylabel(\"\")\n    ax.tick_params(axis=\"both\", which=\"both\", length=0)\n    for spine in ax.spines.values():\n        spine.set_color(GRID_RED)\n        spine.set_alpha(grid_major_alpha)\n        spine.set_linewidth(0.6)\n\n\n# Clinical 3x4 lead order + a full-length Lead II rhythm strip below.\nlead_order = [\"I\", \"aVR\", \"V1\", \"V4\", \"II\", \"aVL\", \"V2\", \"V5\", \"III\", \"aVF\", \"V3\", \"V6\"]\nleads_df = pd.concat(\n    [pd.DataFrame({\"time\": time, \"voltage\": synth_ecg(time, lead_configs[lead]), \"lead\": lead}) for lead in lead_order],\n    ignore_index=True,\n)\nleads_df[\"lead\"] = pd.Categorical(leads_df[\"lead\"], categories=lead_order, ordered=True)\n\nrhythm_duration = duration * 4\nrhythm_time = np.linspace(0, rhythm_duration, int(sampling_rate * rhythm_duration))\nrhythm_df = pd.DataFrame({\"time\": rhythm_time, \"voltage\": synth_ecg(rhythm_time, lead_configs[\"II\"])})\n\n# Plot - exact 3200x1800 canvas; an outer gridspec carves a 3x4 facet block and a\n# full-width rhythm strip into two subfigures, each drawn by the seaborn objects API.\nfig = plt.figure(figsize=(8, 4.5), dpi=400)\nfig.set_facecolor(PAGE_BG)\nouter = fig.add_gridspec(2, 1, height_ratios=[3.05, 1.0], left=0.025, right=0.99, top=0.905, bottom=0.06, hspace=0.10)\nsf_leads = fig.add_subfigure(outer[0])\nsf_rhythm = fig.add_subfigure(outer[1])\nsf_leads.set_facecolor(PAGE_BG)\nsf_rhythm.set_facecolor(PAGE_BG)\n\n# 12 leads as a native seaborn facet grid via the objects interface.\n(\n    so.Plot(leads_df, x=\"time\", y=\"voltage\")\n    .facet(col=\"lead\", order=lead_order, wrap=4)\n    .add(so.Line(color=BRAND, linewidth=0.9))\n    .limit(x=(0, duration), y=(-1.8, 2.0))\n    .label(x=\"\", y=\"\", title=\"\")\n    .share(x=True, y=True)\n    .on(sf_leads)\n    .plot()\n)\n\nlabel_bbox = {\"boxstyle\": \"square,pad=0.18\", \"facecolor\": PAGE_BG, \"edgecolor\": \"none\", \"alpha\": 0.75}\nsf_leads.subplots_adjust(wspace=0.05, hspace=0.16)\nfor ax, lead in zip(sf_leads.axes, lead_order, strict=True):\n    style_ecg_grid(ax, duration, 0.2, 0.04)\n    ax.text(\n        0.03,\n        0.94,\n        lead,\n        transform=ax.transAxes,\n        fontsize=10,\n        fontweight=\"bold\",\n        color=INK,\n        va=\"top\",\n        zorder=10,\n        bbox=label_bbox,\n    )\n\n# 1 mV calibration pulse in the first panel, parked low-left clear of the trace.\ncal_ax = sf_leads.axes[0]\ncal_x0, cal_w = 0.04, 0.10\ncal_ax.plot(\n    [cal_x0, cal_x0, cal_x0 + cal_w, cal_x0 + cal_w], [-1.45, -0.45, -0.45, -1.45], color=INK, linewidth=1.2, zorder=8\n)\ncal_ax.text(cal_x0 + cal_w + 0.10, -1.30, \"1 mV\", fontsize=7.5, ha=\"left\", va=\"center\", color=INK_SOFT)\n\n# Rhythm strip - Lead II running across the full width, also drawn by the objects API.\nax_rhythm = sf_rhythm.subplots()\n(\n    so.Plot(rhythm_df, x=\"time\", y=\"voltage\")\n    .add(so.Line(color=BRAND, linewidth=0.8))\n    .limit(x=(0, rhythm_duration), y=(-1.8, 2.0))\n    .label(x=\"\", y=\"\")\n    .on(ax_rhythm)\n    .plot()\n)\nstyle_ecg_grid(ax_rhythm, rhythm_duration, 1.0, 0.2)\nax_rhythm.text(\n    0.006,\n    0.92,\n    \"II  ·  rhythm strip\",\n    transform=ax_rhythm.transAxes,\n    fontsize=10,\n    fontweight=\"bold\",\n    color=INK,\n    va=\"top\",\n    zorder=10,\n    bbox=label_bbox,\n)\n\n# Title and scale footer\nfig.suptitle(\"ecg-twelve-lead · python · seaborn · anyplot.ai\", fontsize=14, fontweight=\"medium\", color=INK, y=0.965)\nfig.text(0.99, 0.018, \"25 mm/s   ·   10 mm/mV\", fontsize=8, ha=\"right\", va=\"bottom\", color=INK_MUTED)\n\n# Save - exact 3200x1800 (8x4.5 in @ 400 dpi); no bbox_inches so canvas stays on target\nfig.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}