{"spec_id":"ecg-twelve-lead","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' ecg-twelve-lead: ECG/EKG 12-Lead Waveform Display\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 93/100 | Created: 2026-06-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME    <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG  <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK      <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# Imprint palette — first series is always brand green; the ECG trace is the\n# single data series, so it renders in position 1 (#009E73).\nTRACE <- \"#009E73\"\n\n# ECG paper grid uses the classic medical red/pink, derived from the Imprint\n# matte-red anchor (#AE3030). Lighter on the dark surface so it stays visible.\nGRID_COL    <- if (THEME == \"light\") \"#AE3030\" else \"#D98C8C\"\nMINOR_ALPHA <- if (THEME == \"light\") 0.16 else 0.24\nMAJOR_ALPHA <- if (THEME == \"light\") 0.38 else 0.50\n\n# --- ECG model --------------------------------------------------------------\n# Standard scale: 25 mm/s (time) and 10 mm/mV (voltage). We work in millimetre\n# coordinates so that one small grid box (1 mm) equals 0.04 s and 0.1 mV, and\n# coord_fixed keeps the boxes square — the authentic ECG-paper look.\nMM_PER_S  <- 25      # horizontal calibration\nMM_PER_MV <- 10      # vertical calibration\nRR        <- 60 / 72 # normal sinus rhythm, ~72 bpm\n\n# Single-beat P-QRS-T morphology built from Gaussian components. The phase\n# wraps with the RR interval so every beat in a strip is identical.\nstrip_t <- seq(0, 2.5, length.out = 2500)   # 2.5 s per cell at 1000 Hz\nstrip_ph <- ((strip_t - 0.30 + RR / 2) %% RR) - RR / 2\n\n# Per-lead wave amplitudes (mV) reproduce normal precordial/limb morphology:\n# rS pattern in V1-V2, tall R in V4-V6, fully inverted aVR, etc.\nleads <- tibble::tribble(\n    ~lead, ~row, ~col, ~p, ~q, ~r, ~s, ~tw,\n    \"I\", 1L, 1L, 0.10, -0.04, 0.85, -0.10, 0.22,\n    \"aVR\", 1L, 2L, -0.10, 0.00, -0.75, 0.00, -0.22,\n    \"V1\", 1L, 3L, 0.08, 0.00, 0.30, -0.85, -0.12,\n    \"V4\", 1L, 4L, 0.12, -0.08, 1.45, -0.30, 0.38,\n    \"II\", 2L, 1L, 0.16, -0.04, 1.20, -0.10, 0.32,\n    \"aVL\", 2L, 2L, 0.06, -0.03, 0.48, -0.16, 0.13,\n    \"V2\", 2L, 3L, 0.10, 0.00, 0.45, -1.25, 0.24,\n    \"V5\", 2L, 4L, 0.12, -0.10, 1.35, -0.22, 0.36,\n    \"III\", 3L, 1L, 0.09, 0.00, 0.62, -0.05, 0.16,\n    \"aVF\", 3L, 2L, 0.13, -0.03, 0.88, -0.08, 0.24,\n    \"V3\", 3L, 3L, 0.12, -0.05, 0.85, -0.85, 0.30,\n    \"V6\", 3L, 4L, 0.10, -0.08, 1.15, -0.16, 0.32\n)\n\nrow_center <- c(128, 86, 44)        # mm, top row highest\ncol_x <- function(c) (c - 1) * (2.5 * MM_PER_S)  # 62.5 mm per cell\n\n# Build the 12 lead strips into one long data frame.\nstrip_list <- vector(\"list\", nrow(leads))\nfor (i in seq_len(nrow(leads))) {\n    lp <- leads[i, ]\n    wave <- lp$p * exp(-0.5 * ((strip_ph + 0.190) / 0.022)^2) +\n        lp$q * exp(-0.5 * ((strip_ph + 0.025) / 0.0085)^2) +\n        lp$r * exp(-0.5 * ((strip_ph + 0.000) / 0.011)^2) +\n        lp$s * exp(-0.5 * ((strip_ph - 0.028) / 0.011)^2) +\n        lp$tw * exp(-0.5 * ((strip_ph - 0.200) / 0.045)^2)\n    wave <- wave + rnorm(length(strip_t), 0, 0.008)\n    strip_list[[i]] <- tibble::tibble(\n        x = col_x(lp$col) + strip_t * MM_PER_S,\n        y = row_center[lp$row] + wave * MM_PER_MV,\n        lead = lp$lead\n    )\n}\necg <- bind_rows(strip_list)\n\n# Full-length Lead II rhythm strip across the bottom (10 s continuous).\nrhythm_t <- seq(0, 10, length.out = 10000)\nrhythm_ph <- ((rhythm_t - 0.30 + RR / 2) %% RR) - RR / 2\nrhythm_wave <- 0.16 * exp(-0.5 * ((rhythm_ph + 0.190) / 0.022)^2) +\n    -0.04 * exp(-0.5 * ((rhythm_ph + 0.025) / 0.0085)^2) +\n    1.20 * exp(-0.5 * ((rhythm_ph + 0.000) / 0.011)^2) +\n    -0.10 * exp(-0.5 * ((rhythm_ph - 0.028) / 0.011)^2) +\n    0.32 * exp(-0.5 * ((rhythm_ph - 0.200) / 0.045)^2)\nrhythm_wave <- rhythm_wave + rnorm(length(rhythm_t), 0, 0.008)\nrhythm <- tibble::tibble(\n    x = rhythm_t * MM_PER_S,\n    y = 12 + rhythm_wave * MM_PER_MV,\n    lead = \"II\"\n)\n\n# 1 mV calibration pulses (a 10 mm step) at the left margin of every row.\ncal_centers <- c(row_center, 12)\ncal_list <- vector(\"list\", length(cal_centers))\nfor (i in seq_along(cal_centers)) {\n    cal_list[[i]] <- tibble::tibble(\n        x = c(-9, -6, -6, -3, -3, -0.5),\n        y = cal_centers[i] + c(0, 0, 10, 10, 0, 0),\n        grp = i\n    )\n}\ncal <- bind_rows(cal_list)\n\n# Lead name labels at the top-left of each cell.\nlabels <- tibble::tibble(\n    x = col_x(leads$col) + 2,\n    y = row_center[leads$row] + 16,\n    lead = leads$lead\n)\nlabels <- bind_rows(labels, tibble::tibble(x = 2, y = 12 + 16, lead = \"II\"))\n\n# Grid coordinates (1 mm minor, 5 mm major) spanning the full display.\nx_lo <- -11\nx_hi <- 251\ny_lo <- -3\ny_hi <- 151\nminor_x <- seq(-10, 250, by = 1)\nminor_y <- seq(0, 150, by = 1)\nmajor_x <- seq(-10, 250, by = 5)\nmajor_y <- seq(0, 150, by = 5)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot() +\n    geom_vline(xintercept = minor_x, color = GRID_COL, linewidth = 0.12, alpha = MINOR_ALPHA) +\n    geom_hline(yintercept = minor_y, color = GRID_COL, linewidth = 0.12, alpha = MINOR_ALPHA) +\n    geom_vline(xintercept = major_x, color = GRID_COL, linewidth = 0.30, alpha = MAJOR_ALPHA) +\n    geom_hline(yintercept = major_y, color = GRID_COL, linewidth = 0.30, alpha = MAJOR_ALPHA) +\n    geom_path(data = cal, aes(x = x, y = y, group = grp), color = INK, linewidth = 0.5) +\n    geom_path(data = ecg, aes(x = x, y = y, group = lead), color = TRACE, linewidth = 0.42) +\n    geom_path(data = rhythm, aes(x = x, y = y), color = TRACE, linewidth = 0.42) +\n    geom_text(\n        data = labels, aes(x = x, y = y, label = lead),\n        color = INK, fontface = \"bold\", size = 4, hjust = 0\n    ) +\n    labs(\n        title = \"ecg-twelve-lead · r · ggplot2 · anyplot.ai\",\n        subtitle = \"Synthetic normal sinus rhythm (~72 bpm) · 25 mm/s · 10 mm/mV · 1 mV calibration\"\n    ) +\n    coord_fixed(ratio = 1, xlim = c(x_lo, x_hi), ylim = c(y_lo, y_hi), expand = FALSE) +\n    theme_void(base_size = 8) +\n    theme(\n        plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),\n        panel.background = element_rect(fill = PAGE_BG, color = NA),\n        plot.title = element_text(color = INK, size = 13, face = \"bold\", hjust = 0.5,\n                                  margin = margin(b = 4)),\n        plot.subtitle = element_text(color = INK_SOFT, size = 8.5, hjust = 0.5,\n                                     margin = margin(b = 8)),\n        plot.margin = margin(14, 18, 14, 18)\n    )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n    filename = sprintf(\"plot-%s.png\", THEME),\n    plot = p,\n    device = ragg::agg_png,\n    width = 8,\n    height = 4.5,\n    units = \"in\",\n    dpi = 400\n)\n"}