{"spec_id":"burndown-sprint","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nburndown-sprint: Agile Sprint Burndown Chart\nLibrary: plotnine 0.15.7 | Python 3.13.13\nQuality: 91/100 | Created: 2026-06-14\n\"\"\"\n\nimport sys\n\n\nsys.path.pop(0)  # prevent this file (plotnine.py) from shadowing the plotnine library\n\nimport os\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_rect,\n    geom_ribbon,\n    geom_step,\n    geom_vline,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens — Imprint palette chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"\n\n# Sprint data: 10 working days (Jan 8–19 2024), days 0–11 include weekends\n# Day 4 (Jan 12, Fri): scope +8 pushes remaining up from expected ~25 to 33\n# Days 5–6 (Jan 13–14, Sat–Sun): weekend, remaining stays flat\ndays = list(range(12))\nremaining = [40, 36, 33, 29, 33, 33, 33, 27, 21, 15, 8, 0]\nideal = [round(40 * (1 - d / 11), 2) for d in range(12)]\n\nactual_df = pd.DataFrame({\"day\": days, \"value\": remaining, \"series\": \"Actual Remaining\"})\nideal_df = pd.DataFrame({\"day\": days, \"value\": ideal, \"series\": \"Ideal Burndown\"})\nweekend_df = pd.DataFrame({\"xmin\": [4.5], \"xmax\": [6.5], \"ymin\": [-2.0], \"ymax\": [50.0]})\n\n# Behind/ahead-of-schedule ribbon: fills gap between actual and ideal lines\nribbon_df = pd.DataFrame(\n    {\n        \"day\": days,\n        \"ymin\": [min(a, i) for a, i in zip(remaining, ideal, strict=True)],\n        \"ymax\": [max(a, i) for a, i in zip(remaining, ideal, strict=True)],\n    }\n)\n\nx_breaks = [0, 2, 4, 7, 9, 11]\nx_labels = [\"Jan 8\\n(Mon)\", \"Jan 10\\n(Wed)\", \"Jan 12\\n(Fri)\", \"Jan 15\\n(Mon)\", \"Jan 17\\n(Wed)\", \"Jan 19\\n(Fri)\"]\n\nACTUAL_COLOR = IMPRINT_PALETTE[0]  # #009E73 — Imprint position 1, always first series\nIDEAL_COLOR = INK_SOFT  # theme-adaptive muted reference line\n\ntitle = \"burndown-sprint · python · plotnine · anyplot.ai\"\n\nplot = (\n    ggplot()\n    # Weekend shading band — drawn first, behind all data layers\n    + geom_rect(\n        data=weekend_df, mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"), fill=INK_MUTED, alpha=0.15\n    )\n    # Behind/ahead-of-schedule ribbon: translucent fill shows the gap\n    + geom_ribbon(data=ribbon_df, mapping=aes(x=\"day\", ymin=\"ymin\", ymax=\"ymax\"), fill=ANYPLOT_AMBER, alpha=0.12)\n    # Ideal burndown: straight dashed reference from 40 → 0\n    + geom_line(data=ideal_df, mapping=aes(x=\"day\", y=\"value\", color=\"series\"), linetype=\"dashed\", size=0.9)\n    # Actual burndown: step series (work completed in discrete chunks)\n    + geom_step(data=actual_df, mapping=aes(x=\"day\", y=\"value\", color=\"series\"), size=1.4)\n    # Scope change marker: amber dotted vline on day 4 (Jan 12)\n    + geom_vline(xintercept=4.0, linetype=\"dotted\", color=ANYPLOT_AMBER, size=0.8)\n    # Scope change label (placed left of marker)\n    + annotate(\"text\", x=3.8, y=40.5, label=\"Scope\\n+8 pts\", color=ANYPLOT_AMBER, size=3.8, ha=\"right\", va=\"top\")\n    # Weekend label inside the shaded band\n    + annotate(\"text\", x=5.5, y=20.0, label=\"Weekend\", color=INK_MUTED, size=3.5, ha=\"center\", va=\"center\")\n    # Color scale for legend\n    + scale_color_manual(\n        values={\"Actual Remaining\": ACTUAL_COLOR, \"Ideal Burndown\": IDEAL_COLOR},\n        breaks=[\"Actual Remaining\", \"Ideal Burndown\"],\n        name=\"\",\n    )\n    + scale_x_continuous(breaks=x_breaks, labels=x_labels)\n    + scale_y_continuous(breaks=[0, 10, 20, 30, 40])\n    # Clip view so weekend rect (ymin=-2, ymax=50) stays within the axes\n    + coord_cartesian(xlim=(-0.5, 11.5), ylim=(0, 43))\n    + labs(x=\"Sprint Day\", y=\"Remaining Story Points\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.12),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.4),\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),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT, size=8),\n        legend_title=element_blank(),\n        legend_position=\"bottom\",\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}