{"spec_id":"ecg-twelve-lead","library":"makie","language":"julia","code":"# anyplot.ai\n# ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 93/100 | Created: 2026-06-17\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -----------------------------------------------------------\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint palette position 1 (brand green) is the ECG trace — the first series.\nconst TRACE    = colorant\"#009E73\"\n# Imprint matte red (#AE3030) renders the classic ECG paper grid; the theme\n# background stays #FAF8F1 / #1A1A17 per the style guide.\nconst GRID_RED   = colorant\"#AE3030\"\nconst MINOR_A    = THEME == \"light\" ? 0.18 : 0.24\nconst MAJOR_A    = THEME == \"light\" ? 0.42 : 0.50\nconst GRID_MINOR = RGBAf(GRID_RED.r, GRID_RED.g, GRID_RED.b, MINOR_A)\nconst GRID_MAJOR = RGBAf(GRID_RED.r, GRID_RED.g, GRID_RED.b, MAJOR_A)\n\n# --- Synthetic ECG model ----------------------------------------------------\n# One P-QRS-T complex as a sum of Gaussians, in mV, centred on the R peak.\ngauss(t, a, mu, sig) = a * exp(-((t - mu)^2) / (2 * sig^2))\n\nbeat(t, p) =\n    gauss(t, p.P, -0.200, 0.025) +\n    gauss(t, p.Q, -0.025, 0.012) +\n    gauss(t, p.R,  0.000, 0.013) +\n    gauss(t, p.S,  0.028, 0.015) +\n    gauss(t, p.T,  0.180, 0.042)\n\n# Per-lead morphology — normal sinus rhythm with standard polarities and the\n# precordial R/S progression (deep S in V1/V2 → tall R in V4–V6); aVR inverted.\nconst LEAD_PARAMS = Dict(\n    \"I\"   => (P =  0.10, Q = -0.05, R =  0.70, S = -0.12, T =  0.20),\n    \"II\"  => (P =  0.15, Q = -0.05, R =  1.10, S = -0.15, T =  0.30),\n    \"III\" => (P =  0.07, Q = -0.04, R =  0.45, S = -0.10, T =  0.14),\n    \"aVR\" => (P = -0.10, Q =  0.05, R = -0.55, S =  0.10, T = -0.18),\n    \"aVL\" => (P =  0.06, Q = -0.05, R =  0.40, S = -0.08, T =  0.12),\n    \"aVF\" => (P =  0.10, Q = -0.04, R =  0.60, S = -0.12, T =  0.18),\n    \"V1\"  => (P =  0.08, Q =  0.00, R =  0.25, S = -0.80, T =  0.12),\n    \"V2\"  => (P =  0.10, Q =  0.00, R =  0.45, S = -1.00, T =  0.28),\n    \"V3\"  => (P =  0.10, Q = -0.05, R =  0.75, S = -0.70, T =  0.32),\n    \"V4\"  => (P =  0.12, Q = -0.08, R =  1.25, S = -0.35, T =  0.36),\n    \"V5\"  => (P =  0.12, Q = -0.08, R =  1.10, S = -0.20, T =  0.30),\n    \"V6\"  => (P =  0.10, Q = -0.06, R =  0.85, S = -0.12, T =  0.25),\n)\n\n# 1000 Hz sampling, ~75 bpm (RR = 0.80 s). Returns time (s) and voltage (mV).\nfunction ecg_signal(duration, p; fs = 1000, rr = 0.80, first_r = 0.35)\n    t = collect(0:1/fs:duration)\n    v = zeros(length(t))\n    for r0 in first_r:rr:duration\n        v .+= beat.(t .- r0, Ref(p))\n    end\n    v .+= 0.006 .* randn(length(t))   # faint measurement noise\n    return t, v\nend\n\n# --- Display geometry (millimetres: 25 mm/s horizontal, 10 mm/mV vertical) ---\nconst MM_PER_S  = 25.0\nconst MM_PER_MV = 10.0\nconst grid_layout = [\n    (\"I\", \"aVR\", \"V1\", \"V4\"),\n    (\"II\", \"aVL\", \"V2\", \"V5\"),\n    (\"III\", \"aVF\", \"V3\", \"V6\"),\n]\nconst row_centers = [124.0, 96.0, 68.0]            # top → bottom\nconst col_starts  = [14.0, 76.25, 138.5, 200.75]   # 2.5 s (62.5 mm) per column\nconst X_MAX = 265.0\nconst Y_MAX = 150.0\n\n# --- Figure -----------------------------------------------------------------\nfig = Figure(resolution = (1600, 900), fontsize = 14, backgroundcolor = PAGE_BG)\n\nax = Axis(\n    fig[1, 1];\n    title           = \"ecg-twelve-lead · julia · makie · anyplot.ai\",\n    titlesize       = 26,\n    titlecolor      = INK,\n    titlegap        = 14,\n    backgroundcolor = PAGE_BG,\n    aspect          = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\nlimits!(ax, -1, X_MAX + 1, -2, Y_MAX)\n\n# ECG paper grid: light lines every 1 mm, bold lines every 5 mm.\nvlines!(ax, collect(0:1:X_MAX);  color = GRID_MINOR, linewidth = 0.4)\nhlines!(ax, collect(0:1:Y_MAX);  color = GRID_MINOR, linewidth = 0.4)\nvlines!(ax, collect(0:5:X_MAX);  color = GRID_MAJOR, linewidth = 0.9)\nhlines!(ax, collect(0:5:Y_MAX);  color = GRID_MAJOR, linewidth = 0.9)\n\n# 12 leads in the standard 3×4 clinical grid, each with a 1 mV calibration pulse.\nfor (ri, leads) in enumerate(grid_layout)\n    yc = row_centers[ri]\n    cal_x = [3.0, 5.0, 5.0, 9.0, 9.0, 11.0]\n    cal_y = [yc, yc, yc + MM_PER_MV, yc + MM_PER_MV, yc, yc]\n    lines!(ax, cal_x, cal_y; color = TRACE, linewidth = 1.4)\n    for (ci, lead) in enumerate(leads)\n        cs = col_starts[ci]\n        t, v = ecg_signal(2.5, LEAD_PARAMS[lead])\n        lines!(ax, cs .+ t .* MM_PER_S, yc .+ v .* MM_PER_MV;\n            color = TRACE, linewidth = 1.2)\n        text!(ax, cs + 1.5, yc + 13; text = lead, color = INK,\n            fontsize = 21, font = :bold, align = (:left, :bottom))\n    end\nend\n\n# Full-length Lead II rhythm strip across the bottom (10 s = 250 mm).\nyr = 28.0\ncal_x = [3.0, 5.0, 5.0, 9.0, 9.0, 11.0]\nlines!(ax, cal_x, [yr, yr, yr + MM_PER_MV, yr + MM_PER_MV, yr, yr];\n    color = TRACE, linewidth = 1.4)\nt_rhythm, v_rhythm = ecg_signal(10.0, LEAD_PARAMS[\"II\"])\nlines!(ax, 14.0 .+ t_rhythm .* MM_PER_S, yr .+ v_rhythm .* MM_PER_MV;\n    color = TRACE, linewidth = 1.2)\ntext!(ax, 15.5, yr + 13; text = \"II\", color = INK,\n    fontsize = 21, font = :bold, align = (:left, :bottom))\n\n# Scale reference.\ntext!(ax, X_MAX, 2; text = \"25 mm/s    ·    10 mm/mV    ·    1 mV calibration\",\n    color = INK_SOFT, fontsize = 15, align = (:right, :bottom))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}