{"spec_id":"curve-power-duration","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncurve-power-duration: Mean-Maximal Power Duration Curve\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 86/100 | Created: 2026-06-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Script filename shadows the installed `pygal` package when run as `python pygal.py`;\n# dropping the script directory from sys.path lets the real package resolve.\nsys.path.pop(0)\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\nfrom scipy.interpolate import PchipInterpolator\n\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Critical power model — well-trained road cyclist\nCP = 280  # W, critical power (aerobic asymptote)\nW_PRIME = 20000  # J, finite anaerobic work capacity\n\n# X-axis: 15 log-spaced durations as categorical positions (simulates log x-axis)\nDURATIONS_S = [1, 2, 5, 10, 20, 30, 60, 120, 300, 600, 1200, 1800, 3600, 7200, 18000]\nX_LABELS_ALL = [\n    \"1s\",\n    \"2s\",\n    \"5s\",\n    \"10s\",\n    \"20s\",\n    \"30s\",\n    \"1min\",\n    \"2min\",\n    \"5min\",\n    \"10min\",\n    \"20min\",\n    \"30min\",\n    \"1h\",\n    \"2h\",\n    \"5h\",\n]\n# Key reference durations shown as major axis tick labels\nX_LABELS_MAJOR = [\"1s\", \"5s\", \"30s\", \"1min\", \"5min\", \"20min\", \"1h\", \"5h\"]\n\n# Reference duration indices in DURATIONS_S: 5s=2, 1min=6, 5min=8, 20min=10\nREF_IDX = {2, 6, 8, 10}\n\n# Data\n# Empirical reference points (physiology-informed, CP≈280 W, P_max≈1100 W at 1 s)\n_ref_dur = np.array([1, 5, 15, 30, 60, 180, 300, 600, 1200, 1800, 3600, 7200, 18000])\n_ref_pow = np.array([1100, 940, 800, 720, 600, 450, 390, 345, 310, 298, 285, 278, 274])\n\n# Monotone cubic interpolation — preserves non-increasing shape of the curve\n_pchip = PchipInterpolator(np.log10(_ref_dur), _ref_pow)\n_log_dur = np.log10(np.array(DURATIONS_S, dtype=float))\nempirical_power = [round(float(_pchip(ld)), 1) for ld in _log_dur]\n\n# CP model: P(t) = CP + W'/t — only plotted for t ≥ 30 s (where the model is valid)\nmodel_power = [round(CP + W_PRIME / d, 1) if d >= 30 else None for d in DURATIONS_S]\n\n# Horizontal CP asymptote at critical power\ncp_line = [float(CP)] * len(DURATIONS_S)\n\n# Reference duration markers: isolated dots at y_max simulate vertical guide marks.\n# With allow_interruptions=True, each isolated non-None value renders as a lone dot.\nY_MAX = 1150\nref_markers = [Y_MAX if i in REF_IDX else None for i in range(len(DURATIONS_S))]\n\n# Plot\ntitle_str = \"curve-power-duration · python · pygal · anyplot.ai\"\nn_chars = len(title_str)\ntitle_fs = max(44, round(66 * (67 / n_chars if n_chars > 67 else 1.0)))\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=title_fs,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nchart = pygal.Line(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title_str,\n    x_title=\"Duration\",\n    y_title=\"Power (W)\",\n    show_dots=True,\n    dots_size=10,\n    stroke=True,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=30,\n    x_labels_major=X_LABELS_MAJOR,\n    show_minor_x_labels=False,\n    show_y_guides=True,\n    show_x_guides=False,\n    range=(240, 1150),\n    allow_interruptions=True,\n)\nchart.x_labels = X_LABELS_ALL\n\n# Empirical mean-maximal curve — Imprint brand green, primary series with dots\nchart.add(\"Mean-Maximal Power\", empirical_power)\n\n# CP model overlay — Imprint lavender, dashed, starts at 30 s\nchart.add(\"CP Model (P = CP + W’/t)\", model_power, stroke_style={\"width\": 4, \"dasharray\": \"14, 7\"}, show_dots=False)\n\n# Critical power asymptote — Imprint blue, dotted horizontal reference\nchart.add(f\"Critical Power: CP = {CP} W\", cp_line, stroke_style={\"width\": 3, \"dasharray\": \"5, 12\"}, show_dots=False)\n\n# Reference duration markers — Imprint amber (#BD8233), large dots at y_max\n# Positioned at 5s (sprint), 1min, 5min, 20min (FTP proxy) per spec requirement\nchart.add(\"Reference Durations (5 s · 1 min · 5 min · 20 min)\", ref_markers, show_dots=True, dots_size=18)\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}