{"spec_id":"line-training-load-pmc","library":"makie","language":"julia","code":"# anyplot.ai\n# line-training-load-pmc: Training Load Performance Management Chart\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 93/100 | Created: 2026-06-13\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens (Imprint chrome)\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\n# Imprint palette — semantic assignment for PMC series\nconst C_CTL     = colorant\"#009E73\"  # pos 1, brand green  — CTL / fitness (first series)\nconst C_ATL     = colorant\"#C475FD\"  # pos 2, lavender     — ATL / fatigue\nconst C_TSS     = colorant\"#4467A3\"  # pos 3, blue         — daily TSS bars\nconst C_TSB_POS = colorant\"#009E73\"  # green (semantic: fresh / positive form)\nconst C_TSB_NEG = colorant\"#AE3030\"  # pos 5, matte red    — fatigued / negative form\n\n# Data: 180-day cycling season (Jan–Jun 2026) — base build, peak, taper\nn   = 180\ntss = zeros(Float64, n)\n\nfor i in 1:n\n    dow = (i - 1) % 7   # 0=Mon … 6=Sun\n    if i <= 60           # Base: aerobic foundation\n        if dow < 5\n            tss[i] = max(0.0, 55.0 + 20.0 * randn())\n        else\n            tss[i] = max(0.0, 15.0 + 6.0 * randn())\n        end\n        dow == 5 && (tss[i] += 35.0)\n    elseif i <= 120      # Build: 3 hard weeks + 1 recovery per block\n        if (i - 1) % 28 >= 21\n            tss[i] = max(0.0, 30.0 + 12.0 * randn())   # recovery week\n        elseif dow < 5\n            tss[i] = max(0.0, 78.0 + 28.0 * randn())\n        else\n            tss[i] = max(0.0, 18.0 + 8.0 * randn())\n        end\n        (dow == 5 && (i - 1) % 28 < 21) && (tss[i] += 55.0)\n    elseif i <= 150      # Peak: race-prep intensity blocks\n        if dow < 5\n            tss[i] = max(0.0, 90.0 + 32.0 * randn())\n        else\n            tss[i] = max(0.0, 20.0 + 10.0 * randn())\n        end\n        dow == 5 && (tss[i] += 65.0)\n    else                 # Taper: cut volume for peak race\n        taper = 1.0 - 0.7 * (i - 150) / 30.0\n        if dow < 5\n            tss[i] = max(0.0, (62.0 + 18.0 * randn()) * taper)\n        else\n            tss[i] = max(0.0, (14.0 + 5.0 * randn()) * taper)\n        end\n    end\nend\n\n# Exponentially weighted moving averages (TrainingPeaks standard formulas)\nalpha_ctl = 1.0 - exp(-1.0 / 42.0)   # 42-day time constant → chronic fitness\nalpha_atl = 1.0 - exp(-1.0 / 7.0)    # 7-day time constant  → acute fatigue\n\nctl = zeros(Float64, n)\natl = zeros(Float64, n)\ntsb = zeros(Float64, n)\n\nctl[1] = tss[1] * alpha_ctl\natl[1] = tss[1] * alpha_atl\n\nfor i in 2:n\n    tsb[i] = ctl[i-1] - atl[i-1]                          # form = yesterday's fitness − fatigue\n    ctl[i] = ctl[i-1] + alpha_ctl * (tss[i] - ctl[i-1])\n    atl[i] = atl[i-1] + alpha_atl * (tss[i] - atl[i-1])\nend\n\ndays    = collect(1:n)\ntsb_pos = [max(0.0, v) for v in tsb]\ntsb_neg = [min(0.0, v) for v in tsb]\n\nmonth_ticks = ([1, 32, 60, 91, 121, 152], [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"])\n\n# Figure: landscape canvas → 3200 × 1800 px at px_per_unit = 2\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\n# Top panel — CTL, ATL, TSS\nax1 = Axis(\n    fig[1, 1];\n    backgroundcolor    = PAGE_BG,\n    title              = \"line-training-load-pmc · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    ylabel             = \"Training Load (TSS units)\",\n    ylabelsize         = 14,\n    ylabelcolor        = INK,\n    yticklabelsize     = 12,\n    yticklabelcolor    = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    xticklabelsvisible = false,\n    xlabelvisible      = false,\n    xtickcolor         = INK_SOFT,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n    xticks             = month_ticks,\n)\n\n# Bottom panel — TSB / Form\nax2 = Axis(\n    fig[2, 1];\n    backgroundcolor   = PAGE_BG,\n    xlabel            = \"Date (Jan–Jun 2026, daily)\",\n    ylabel            = \"Form / TSB\",\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    leftspinecolor    = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    topspinevisible   = false,\n    rightspinevisible = false,\n    xgridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    ygridvisible      = false,\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n    xticks            = month_ticks,\n)\n\nrowsize!(fig.layout, 1, Relative(0.63))\nrowsize!(fig.layout, 2, Relative(0.37))\nrowgap!(fig.layout, 1, 6)\n\nlinkxaxes!(ax1, ax2)\n\n# TSS bars — raw daily training stress (background layer, semi-transparent)\nbarplot!(ax1, days, tss;\n    color       = (C_TSS, 0.20),\n    strokewidth = 0.0,\n    width       = 0.9,\n)\n\n# CTL — chronic fitness line (green, thick, smooth)\nlines!(ax1, days, ctl;\n    color     = C_CTL,\n    linewidth = 3.0,\n)\n\n# ATL — acute fatigue line (lavender, dashed)\nlines!(ax1, days, atl;\n    color     = C_ATL,\n    linewidth = 2.5,\n    linestyle = :dash,\n)\n\n# TSB zero reference\nhlines!(ax2, [0.0];\n    color     = (INK, 0.30),\n    linewidth = 1.0,\n    linestyle = :dot,\n)\n\n# TSB filled area — green above zero (fresh), red below zero (fatigued)\nband!(ax2, days, zeros(n), tsb_pos;\n    color = (C_TSB_POS, 0.22),\n)\nband!(ax2, days, tsb_neg, zeros(n);\n    color = (C_TSB_NEG, 0.22),\n)\n\n# TSB outline (subtle ink trace)\nlines!(ax2, days, tsb;\n    color     = (INK, 0.50),\n    linewidth = 1.2,\n)\n\n# Symmetric TSB y-axis so zero sits at midpoint\ntsb_extent = maximum(abs.(tsb)) * 1.35\nylims!(ax2, -tsb_extent, tsb_extent)\n\n# External legend (right column, spans both panels)\nleg_elements = [\n    LineElement(color = C_CTL, linewidth = 3.0),\n    LineElement(color = C_ATL, linewidth = 2.5, linestyle = :dash),\n    PolyElement(color = (C_TSS, 0.40)),\n    PolyElement(color = (C_TSB_POS, 0.50)),\n    PolyElement(color = (C_TSB_NEG, 0.50)),\n]\nleg_labels = [\n    \"CTL – Fitness (42-day avg)\",\n    \"ATL – Fatigue (7-day avg)\",\n    \"TSS – Daily Load\",\n    \"TSB+ – Fresh / Positive Form\",\n    \"TSB– – Fatigued / Negative Form\",\n]\n\nLegend(\n    fig[1:2, 2],\n    leg_elements,\n    leg_labels;\n    framecolor      = INK_SOFT,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor      = INK,\n    labelsize  = 12,\n    patchsize  = (26, 14),\n    framewidth = 0.5,\n    padding    = (10, 10, 10, 10),\n)\n\ncolsize!(fig.layout, 2, Relative(0.22))\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}