{"spec_id":"curve-power-duration","library":"makie","language":"julia","code":"# anyplot.ai\n# curve-power-duration: Mean-Maximal Power Duration Curve\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 90/100 | Created: 2026-06-13\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 ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green (Imprint palette — always first series)\n    colorant\"#C475FD\",  # 2 — lavender\n    colorant\"#4467A3\",  # 3 — blue\n    colorant\"#BD8233\",  # 4 — ochre\n    colorant\"#AE3030\",  # 5 — matte red\n    colorant\"#2ABCCD\",  # 6 — cyan\n    colorant\"#954477\",  # 7 — rose\n    colorant\"#99B314\",  # 8 — lime\n]\n\n# Critical power parameters — well-trained cyclist\nconst CP      = 280.0    # W  — aerobic asymptote (critical power)\nconst W_PRIME = 20_000.0 # J  — anaerobic work capacity (W′)\nconst P_MAX   = 1_100.0  # W  — neuromuscular peak at 1 s\n\n# Empirical mean-maximal power: 45 log-spaced efforts from 1 s to 5 h\nconst n_emp   = 45\nconst dur_emp = exp.(range(log(1.0), log(18_000.0); length = n_emp))\n\n# At short durations the neuromuscular cap (P_MAX) limits power below the CP model;\n# from ~32 s onwards the CP model governs. Use min() to model this crossover.\nbase_emp        = [min(P_MAX * t^(-0.05), CP + W_PRIME / t) for t in dur_emp]\nscatter_weights = [exp(-((log(t) - log(300.0))^2) / (2.0 * log(8.0)^2)) for t in dur_emp]\nempirical_mmp   = base_emp .+ abs.(randn(n_emp)) .* 12.0 .* scatter_weights\n\n# CP model line — dense curve (300 points) for smooth rendering\nconst n_model     = 300\nconst dur_model   = exp.(range(log(1.0), log(18_000.0); length = n_model))\nconst model_power = [CP + W_PRIME / t for t in dur_model]\n\n# Display range — clip the model's off-chart divergence at very short durations\nconst Y_MIN = 200.0\nconst Y_MAX = 1_250.0\nmodel_mask = model_power .<= Y_MAX\nmt = dur_model[model_mask]\nmp = model_power[model_mask]\n\n# Reference durations specified in the spec\nconst ref_dur    = [5.0, 60.0, 300.0, 1_200.0]\nconst ref_labels = [\"5 s\", \"1 min\", \"5 min\", \"20 min (FTP)\"]\n\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = \"curve-power-duration · julia · makie · anyplot.ai\",\n    titlesize         = 20,\n    titlecolor        = INK,\n    xlabel            = \"Duration\",\n    ylabel            = \"Power (W)\",\n    xlabelsize        = 14,\n    ylabelsize        = 14,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelsize    = 12,\n    yticklabelsize    = 12,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = INK_SOFT,\n    backgroundcolor   = PAGE_BG,\n    topspinevisible   = false,\n    rightspinevisible = false,\n    leftspinecolor    = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    xscale            = log10,\n    xgridvisible      = false,\n    ygridvisible      = true,\n    ygridcolor        = (INK, 0.12),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n)\n\nxlims!(ax, 1.0, 18_000.0)\nylims!(ax, Y_MIN, Y_MAX)\n\n# Reference duration vertical lines (drawn first so data layers render on top)\nfor dur in ref_dur\n    vlines!(ax, [dur]; color = (INK_SOFT, 0.45), linewidth = 1.0, linestyle = :dash)\nend\n\n# W′ reserve zone — band! fill between CP asymptote and model line (Makie-native primitive)\nband!(ax, mt, fill(CP, length(mt)), mp;\n    color = (IMPRINT_PALETTE[3], 0.08),\n)\n\n# Area fill under MMP curve — band! adds depth and makes the primary data the focal point\nband!(ax, dur_emp, fill(Y_MIN, n_emp), empirical_mmp;\n    color = (IMPRINT_PALETTE[1], 0.08),\n)\n\n# CP model overlay (dashed, Imprint position 3 — blue)\nlines!(ax, mt, mp;\n    color     = IMPRINT_PALETTE[3],\n    linewidth = 2.5,\n    linestyle = :dash,\n    label     = \"CP model:  P = CP + W′/t\",\n)\n\n# Empirical MMP curve (solid, Imprint position 1 — brand green)\nlines!(ax, dur_emp, empirical_mmp;\n    color     = IMPRINT_PALETTE[1],\n    linewidth = 3.0,\n    label     = \"Mean-maximal power (empirical)\",\n)\nscatter!(ax, dur_emp, empirical_mmp;\n    color       = IMPRINT_PALETTE[1],\n    markersize  = 8,\n    strokewidth = 1,\n    strokecolor = PAGE_BG,\n)\n\n# CP asymptote horizontal line (dotted, muted)\nhlines!(ax, [CP]; color = (INK_MUTED, 0.85), linewidth = 1.5, linestyle = :dot)\n\n# CP asymptote label\ntext!(ax, 150.0, CP + 14;\n    text     = \"CP = $(Int(CP)) W\",\n    color    = INK_MUTED,\n    fontsize = 12,\n    align    = (:left, :bottom),\n)\n\n# Reference duration labels at the top of each vertical marker\nfor (dur, lbl) in zip(ref_dur, ref_labels)\n    text!(ax, dur, Y_MAX - 30;\n        text     = lbl,\n        color    = INK_SOFT,\n        fontsize = 12,\n        align    = (:center, :top),\n    )\nend\n\n# Human-readable x-axis tick labels (log scale, data-space positions)\nax.xticks = (\n    [1.0, 5.0, 30.0, 60.0, 300.0, 1_200.0, 3_600.0, 18_000.0],\n    [\"1 s\", \"5 s\", \"30 s\", \"1 min\", \"5 min\", \"20 min\", \"1 h\", \"5 h\"],\n)\n\naxislegend(ax;\n    position        = :rt,\n    framevisible    = true,\n    framecolor      = (INK_SOFT, 0.4),\n    backgroundcolor = ELEVATED_BG,\n    labelcolor      = INK,\n    padding         = (10, 10, 8, 8),\n    labelsize       = 12,\n    patchsize       = (24, 8),\n    rowgap          = 4,\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}