{"spec_id":"burndown-sprint","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nburndown-sprint: Agile Sprint Burndown Chart\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 88/100 | Created: 2026-06-14\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens\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\"\n\n# Imprint palette — position 1 always first series\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # actual burndown — brand green, always first series\nIDEAL_COLOR = IMPRINT_PALETTE[2]  # ideal guideline — Imprint blue\nSCOPE_COLOR = IMPRINT_PALETTE[4]  # scope-change marker — matte red (semantic: risk/alert)\n# Slightly lighter red for annotation on dark backgrounds to improve contrast\nSCOPE_ANNOTATION_COLOR = \"#AE3030\" if THEME == \"light\" else \"#D45555\"\n\n# Data — 10-working-day sprint, Oct 7–18 2024\n# Initial scope: 40 story points; +8 scope added on day 4 (Oct 11, Fri)\nday_indices = list(range(12))\nday_labels = [\n    \"Oct 7\",\n    \"Oct 8\",\n    \"Oct 9\",\n    \"Oct 10\",\n    \"Oct 11\",\n    \"Oct 12\",\n    \"Oct 13\",\n    \"Oct 14\",\n    \"Oct 15\",\n    \"Oct 16\",\n    \"Oct 17\",\n    \"Oct 18\",\n]\n\n# Actual remaining story points — step series (flat on non-working days)\n# Day 4 (Oct 11): 30 SP remaining, 8 added, 3 burned → 35\nremaining = [40, 36, 33, 30, 35, 35, 35, 28, 22, 16, 10, 4]\n\n# Ideal burndown: linear from 40 → 0 across all 12 calendar days\nideal = [round(40 - 40 * i / 11, 2) for i in range(12)]\n\ndf_actual = pd.DataFrame({\"day\": day_indices, \"value\": remaining, \"series\": \"Actual\"})\ndf_ideal = pd.DataFrame({\"day\": day_indices, \"value\": ideal, \"series\": \"Ideal\"})\n\n# Weekend band (Oct 12–13 = day indices 5–6)\ndf_weekend = pd.DataFrame({\"xmin\": [4.5], \"xmax\": [6.5], \"ymin\": [0.0], \"ymax\": [48.0]})\n\n# Scope change annotation (placed just right of the vertical marker)\ndf_scope_label = pd.DataFrame({\"x\": [4.35], \"y\": [42.0], \"label\": [\"+8 pts scope added\"]})\n\n# Weekend band label (top of the shaded band)\ndf_wknd_label = pd.DataFrame({\"x\": [5.5], \"y\": [45.0], \"label\": [\"Weekend\"]})\n\n# Theme\nanyplot_theme = theme(\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_SOFT, size=0.25),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    panel_border=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_text_x=element_text(color=INK_SOFT, size=9, angle=45, vjust=1, hjust=1),\n    axis_ticks=element_line(color=INK_SOFT),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=16),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_blank(),\n    legend_position=[0.88, 0.82],\n)\n\ntitle = \"burndown-sprint · python · letsplot · anyplot.ai\"\n\nplot = (\n    ggplot()\n    # Weekend background shading (border matches bg so only the fill shows)\n    + geom_rect(\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_weekend,\n        fill=INK_MUTED,\n        alpha=0.15,\n        color=PAGE_BG,\n    )\n    # Weekend label inside the band\n    + geom_text(mapping=aes(x=\"x\", y=\"y\", label=\"label\"), data=df_wknd_label, color=INK_MUTED, size=3.5)\n    # Ideal burndown guideline (dashed, Imprint blue)\n    + geom_line(mapping=aes(x=\"day\", y=\"value\", color=\"series\"), data=df_ideal, size=1.0, linetype=\"dashed\")\n    # Actual burndown as a step series (brand green — first series)\n    + geom_step(mapping=aes(x=\"day\", y=\"value\", color=\"series\"), data=df_actual, size=1.4)\n    # Scope change vertical marker (matte red — semantic alert)\n    + geom_vline(xintercept=4, color=SCOPE_COLOR, size=0.8, linetype=\"dotted\")\n    # Scope change annotation\n    + geom_text(\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"), data=df_scope_label, color=SCOPE_ANNOTATION_COLOR, size=4.0, hjust=0\n    )\n    # Color scale / legend for the two series\n    + scale_color_manual(values={\"Actual\": BRAND, \"Ideal\": IDEAL_COLOR}, name=\"\")\n    + scale_x_continuous(breaks=day_indices, labels=day_labels, expand=[0.02, 0])\n    + scale_y_continuous(limits=[0, 48])\n    + labs(x=\"Sprint Day  (Oct 7 – Oct 18, 2024)\", y=\"Remaining Story Points\", title=title)\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\n# Save PNG + HTML (both themes handled by ANYPLOT_THEME env var)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}