ECG/EKG 12-Lead Waveform Display — Makie.jl

A multi-channel electrocardiogram display showing the 12 standard ECG leads arranged in clinical format. Each lead displays realistic P-QRS-T wave complexes on a medical-standard grid background with calibration markers. This visualization replicates the familiar layout used in hospitals and clinics worldwide, making it immediately recognizable to healthcare professionals and useful for medical education.

ECG/EKG 12-Lead Waveform Display rendered with Makie.jl

Julia source (Makie.jl)

# anyplot.ai
# ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 93/100 | Created: 2026-06-17

using CairoMakie
using Colors
using Random

Random.seed!(42)

# --- Theme tokens -----------------------------------------------------------
const THEME    = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG  = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const INK      = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"

# Imprint palette position 1 (brand green) is the ECG trace — the first series.
const TRACE    = colorant"#009E73"
# Imprint matte red (#AE3030) renders the classic ECG paper grid; the theme
# background stays #FAF8F1 / #1A1A17 per the style guide.
const GRID_RED   = colorant"#AE3030"
const MINOR_A    = THEME == "light" ? 0.18 : 0.24
const MAJOR_A    = THEME == "light" ? 0.42 : 0.50
const GRID_MINOR = RGBAf(GRID_RED.r, GRID_RED.g, GRID_RED.b, MINOR_A)
const GRID_MAJOR = RGBAf(GRID_RED.r, GRID_RED.g, GRID_RED.b, MAJOR_A)

# --- Synthetic ECG model ----------------------------------------------------
# One P-QRS-T complex as a sum of Gaussians, in mV, centred on the R peak.
gauss(t, a, mu, sig) = a * exp(-((t - mu)^2) / (2 * sig^2))

beat(t, p) =
    gauss(t, p.P, -0.200, 0.025) +
    gauss(t, p.Q, -0.025, 0.012) +
    gauss(t, p.R,  0.000, 0.013) +
    gauss(t, p.S,  0.028, 0.015) +
    gauss(t, p.T,  0.180, 0.042)

# Per-lead morphology — normal sinus rhythm with standard polarities and the
# precordial R/S progression (deep S in V1/V2 → tall R in V4–V6); aVR inverted.
const LEAD_PARAMS = Dict(
    "I"   => (P =  0.10, Q = -0.05, R =  0.70, S = -0.12, T =  0.20),
    "II"  => (P =  0.15, Q = -0.05, R =  1.10, S = -0.15, T =  0.30),
    "III" => (P =  0.07, Q = -0.04, R =  0.45, S = -0.10, T =  0.14),
    "aVR" => (P = -0.10, Q =  0.05, R = -0.55, S =  0.10, T = -0.18),
    "aVL" => (P =  0.06, Q = -0.05, R =  0.40, S = -0.08, T =  0.12),
    "aVF" => (P =  0.10, Q = -0.04, R =  0.60, S = -0.12, T =  0.18),
    "V1"  => (P =  0.08, Q =  0.00, R =  0.25, S = -0.80, T =  0.12),
    "V2"  => (P =  0.10, Q =  0.00, R =  0.45, S = -1.00, T =  0.28),
    "V3"  => (P =  0.10, Q = -0.05, R =  0.75, S = -0.70, T =  0.32),
    "V4"  => (P =  0.12, Q = -0.08, R =  1.25, S = -0.35, T =  0.36),
    "V5"  => (P =  0.12, Q = -0.08, R =  1.10, S = -0.20, T =  0.30),
    "V6"  => (P =  0.10, Q = -0.06, R =  0.85, S = -0.12, T =  0.25),
)

# 1000 Hz sampling, ~75 bpm (RR = 0.80 s). Returns time (s) and voltage (mV).
function ecg_signal(duration, p; fs = 1000, rr = 0.80, first_r = 0.35)
    t = collect(0:1/fs:duration)
    v = zeros(length(t))
    for r0 in first_r:rr:duration
        v .+= beat.(t .- r0, Ref(p))
    end
    v .+= 0.006 .* randn(length(t))   # faint measurement noise
    return t, v
end

# --- Display geometry (millimetres: 25 mm/s horizontal, 10 mm/mV vertical) ---
const MM_PER_S  = 25.0
const MM_PER_MV = 10.0
const grid_layout = [
    ("I", "aVR", "V1", "V4"),
    ("II", "aVL", "V2", "V5"),
    ("III", "aVF", "V3", "V6"),
]
const row_centers = [124.0, 96.0, 68.0]            # top → bottom
const col_starts  = [14.0, 76.25, 138.5, 200.75]   # 2.5 s (62.5 mm) per column
const X_MAX = 265.0
const Y_MAX = 150.0

# --- Figure -----------------------------------------------------------------
fig = Figure(resolution = (1600, 900), fontsize = 14, backgroundcolor = PAGE_BG)

ax = Axis(
    fig[1, 1];
    title           = "ecg-twelve-lead · julia · makie · anyplot.ai",
    titlesize       = 26,
    titlecolor      = INK,
    titlegap        = 14,
    backgroundcolor = PAGE_BG,
    aspect          = DataAspect(),
)
hidedecorations!(ax)
hidespines!(ax)
limits!(ax, -1, X_MAX + 1, -2, Y_MAX)

# ECG paper grid: light lines every 1 mm, bold lines every 5 mm.
vlines!(ax, collect(0:1:X_MAX);  color = GRID_MINOR, linewidth = 0.4)
hlines!(ax, collect(0:1:Y_MAX);  color = GRID_MINOR, linewidth = 0.4)
vlines!(ax, collect(0:5:X_MAX);  color = GRID_MAJOR, linewidth = 0.9)
hlines!(ax, collect(0:5:Y_MAX);  color = GRID_MAJOR, linewidth = 0.9)

# 12 leads in the standard 3×4 clinical grid, each with a 1 mV calibration pulse.
for (ri, leads) in enumerate(grid_layout)
    yc = row_centers[ri]
    cal_x = [3.0, 5.0, 5.0, 9.0, 9.0, 11.0]
    cal_y = [yc, yc, yc + MM_PER_MV, yc + MM_PER_MV, yc, yc]
    lines!(ax, cal_x, cal_y; color = TRACE, linewidth = 1.4)
    for (ci, lead) in enumerate(leads)
        cs = col_starts[ci]
        t, v = ecg_signal(2.5, LEAD_PARAMS[lead])
        lines!(ax, cs .+ t .* MM_PER_S, yc .+ v .* MM_PER_MV;
            color = TRACE, linewidth = 1.2)
        text!(ax, cs + 1.5, yc + 13; text = lead, color = INK,
            fontsize = 21, font = :bold, align = (:left, :bottom))
    end
end

# Full-length Lead II rhythm strip across the bottom (10 s = 250 mm).
yr = 28.0
cal_x = [3.0, 5.0, 5.0, 9.0, 9.0, 11.0]
lines!(ax, cal_x, [yr, yr, yr + MM_PER_MV, yr + MM_PER_MV, yr, yr];
    color = TRACE, linewidth = 1.4)
t_rhythm, v_rhythm = ecg_signal(10.0, LEAD_PARAMS["II"])
lines!(ax, 14.0 .+ t_rhythm .* MM_PER_S, yr .+ v_rhythm .* MM_PER_MV;
    color = TRACE, linewidth = 1.2)
text!(ax, 15.5, yr + 13; text = "II", color = INK,
    fontsize = 21, font = :bold, align = (:left, :bottom))

# Scale reference.
text!(ax, X_MAX, 2; text = "25 mm/s    ·    10 mm/mV    ·    1 mV calibration",
    color = INK_SOFT, fontsize = 15, align = (:right, :bottom))

# --- Save -------------------------------------------------------------------
save("plot-$(THEME).png", fig; px_per_unit = 2)

Part of ECG/EKG 12-Lead Waveform Display on anyplot.ai.

Other implementations