{"spec_id":"curve-power-duration","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' curve-power-duration: Mean-Maximal Power Duration Curve\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Created: 2026-06-13\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\nIMPRINT_PALETTE <- c(\n    \"#009E73\",  # 1 - brand green  (empirical MMP curve)\n    \"#C475FD\",  # 2 - lavender     (CP model overlay)\n    \"#4467A3\",  # 3 - blue\n    \"#BD8233\",  # 4 - ochre\n    \"#AE3030\",  # 5 - matte red\n    \"#2ABCCD\",  # 6 - cyan\n    \"#954477\",  # 7 - rose\n    \"#99B314\"   # 8 - lime\n)\n\n# Grid color: INK with alpha embedded for subtlety (ggplot2 has no alpha on element_line)\nGRID_COLOR <- grDevices::adjustcolor(INK, alpha.f = 0.12)\n\n# --- Data -------------------------------------------------------------------\n\n# Well-trained cyclist parameters\nCP   <- 280    # Critical Power (watts) — aerobic asymptote\nWp   <- 20000  # W' anaerobic work capacity (joules)\nPmax <- 1100   # Neuromuscular peak power at 1 s (watts)\n\n# Empirical mean-maximal power: 45 log-spaced durations from 1 s to 5 h\nn_emp   <- 45\ndur_emp <- exp(seq(log(1), log(18000), length.out = n_emp))\n\n# Power-law decay: P(t) = CP + (Pmax - CP) / sqrt(t)\n# Gives realistic MMP shape: 1100 W at 1 s, converging to ~286 W at 5 h\nW_eff     <- Pmax - CP   # 820 W effective surplus above CP at t = 1 s\nemp_power <- CP + W_eff / sqrt(dur_emp)\n\n# Add ~2% noise and enforce monotonicity (MMP is always non-increasing)\nnoise      <- rnorm(n_emp, 0, sd = 5)\nemp_final  <- cummin(emp_power + noise)\n\ndf_emp <- data.frame(\n    duration_s = dur_emp,\n    power_w    = emp_final,\n    series     = \"Mean-Maximal Power\"\n)\n\n# CP model P(t) = CP + W'/t — shown from 2 min onward where it is physically valid\n# (at < 2 min the model overestimates vs. neuromuscular-bounded empirical efforts)\ndur_mod   <- exp(seq(log(120), log(18000), length.out = 200))\nmod_power <- CP + Wp / dur_mod\n\ndf_mod <- data.frame(\n    duration_s = dur_mod,\n    power_w    = mod_power,\n    series     = \"Critical Power Model\"\n)\n\n# Reference durations for vertical annotation guides\nref_dur    <- c(5, 60, 300, 1200)\nref_labels <- c(\"5 s\", \"1 min\", \"5 min\", \"20 min\")\n\n# Title (46 chars < 67 baseline — no fontsize scaling needed)\nplot_title <- \"curve-power-duration · r · ggplot2 · anyplot.ai\"\n\n# --- Plot -------------------------------------------------------------------\n\np <- ggplot() +\n    # CP asymptote — horizontal guide at CP = 280 W\n    geom_hline(\n        yintercept = CP,\n        color      = INK_SOFT,\n        linewidth  = 0.5,\n        linetype   = \"dotted\"\n    ) +\n    # Reference duration vertical guides (5 s sprint, 1 min, 5 min, 20 min FTP)\n    geom_vline(\n        xintercept = ref_dur,\n        color      = INK_MUTED,\n        linewidth  = 0.35,\n        linetype   = \"dashed\",\n        alpha      = 0.55\n    ) +\n    # CP model — dashed lavender line (from 2 min to 5 h)\n    geom_line(\n        data      = df_mod,\n        aes(x = duration_s, y = power_w, color = series, linetype = series),\n        linewidth = 1.0\n    ) +\n    # Empirical MMP — solid green line\n    geom_line(\n        data      = df_emp,\n        aes(x = duration_s, y = power_w, color = series, linetype = series),\n        linewidth = 1.3\n    ) +\n    # Empirical data points (every 3rd for clarity at this density)\n    geom_point(\n        data        = df_emp[seq(1, n_emp, by = 3), ],\n        aes(x = duration_s, y = power_w),\n        color       = IMPRINT_PALETTE[1],\n        size        = 1.8,\n        alpha       = 0.75,\n        show.legend = FALSE\n    ) +\n    # Reference duration labels at top of chart\n    annotate(\n        \"text\",\n        x         = ref_dur,\n        y         = 1120,\n        label     = ref_labels,\n        color     = INK_MUTED,\n        size      = 2.4,\n        hjust     = 0.5,\n        vjust     = 1,\n        fontface  = \"plain\"\n    ) +\n    # CP label next to the asymptote line\n    annotate(\n        \"text\",\n        x        = 160,\n        y        = CP + 24,\n        label    = \"CP = 280 W\",\n        color    = INK_SOFT,\n        size     = 2.6,\n        hjust    = 0,\n        fontface = \"italic\"\n    ) +\n    # Color scale — Imprint positions 1 (green) and 2 (lavender), canonical order\n    # limits forces legend order: MMP first (primary), CP Model second\n    scale_color_manual(\n        values = c(\n            \"Mean-Maximal Power\"   = IMPRINT_PALETTE[1],\n            \"Critical Power Model\" = IMPRINT_PALETTE[2]\n        ),\n        limits = c(\"Mean-Maximal Power\", \"Critical Power Model\"),\n        name   = NULL\n    ) +\n    # Linetype scale — suppress redundant guide, handled via override in color guide\n    scale_linetype_manual(\n        values = c(\n            \"Mean-Maximal Power\"   = \"solid\",\n            \"Critical Power Model\" = \"dashed\"\n        ),\n        limits = c(\"Mean-Maximal Power\", \"Critical Power Model\"),\n        name   = NULL,\n        guide  = \"none\"\n    ) +\n    # Legend icons match actual styles: MMP = solid green, CP Model = dashed lavender\n    guides(color = guide_legend(\n        override.aes = list(\n            linetype  = c(\"solid\", \"dashed\"),\n            linewidth = c(1.3, 1.0)\n        )\n    )) +\n    # Log-scale x axis: 1 s to 5 h with human-readable labels\n    scale_x_log10(\n        limits = c(1, 18000),\n        breaks = c(1, 5, 30, 60, 300, 1200, 3600, 18000),\n        labels = c(\"1 s\", \"5 s\", \"30 s\", \"1 min\", \"5 min\", \"20 min\", \"1 h\", \"5 h\"),\n        expand = expansion(mult = c(0.02, 0.04))\n    ) +\n    # Y axis: power in watts, 200–1150 W shows full curve and CP asymptote\n    scale_y_continuous(\n        limits = c(200, 1150),\n        breaks = c(200, 400, 600, 800, 1000),\n        labels = function(x) paste0(x, \" W\"),\n        expand = expansion(mult = c(0.02, 0.08))\n    ) +\n    labs(\n        title = plot_title,\n        x     = \"Duration (log scale, s)\",\n        y     = \"Mean-Maximal Power (W)\"\n    ) +\n    theme_minimal(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        panel.grid.major  = element_line(color = GRID_COLOR, linewidth = 0.35),\n        panel.grid.minor  = element_blank(),\n        panel.border      = element_blank(),\n        axis.line         = element_line(color = INK_SOFT, linewidth = 0.4),\n        axis.ticks        = element_line(color = INK_SOFT, linewidth = 0.3),\n        axis.ticks.length = unit(3, \"pt\"),\n        axis.title        = element_text(color = INK,      size = 10),\n        axis.text         = element_text(color = INK_SOFT, size = 8),\n        plot.title        = element_text(color = INK,      size = 12, face = \"bold\",\n                                         margin = margin(b = 8)),\n        legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT,\n                                         linewidth = 0.3),\n        legend.text       = element_text(color = INK_SOFT, size = 8),\n        legend.key.width  = unit(1.4, \"cm\"),\n        legend.position   = \"bottom\",\n        legend.margin     = margin(t = 4, b = 4, l = 6, r = 6),\n        plot.margin       = margin(t = 12, r = 20, b = 8, l = 10)\n    )\n\n# --- Save -------------------------------------------------------------------\n\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"}