{"spec_id":"burndown-sprint","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nburndown-sprint: Agile Sprint Burndown Chart\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 91/100 | Created: 2026-06-14\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: the script is named altair.py, which shadows the library\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir]\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (see prompts/default-style-guide.md)\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\"\nANYPLOT_AMBER = \"#DDCC77\"  # Imprint amber — scope change warning\n\nBRAND = \"#009E73\"  # Imprint palette position 1 — actual remaining (first series)\n\n# Data — 10-working-day sprint Jan 6–17 2025; +8 pt scope change on Jan 9 (Thu)\n# Story: team starts at 40 pts, burns down well, scope jumps on day 4, recovers to finish at 0\nsprint_dates = pd.to_datetime(\n    [\n        \"2025-01-06\",\n        \"2025-01-07\",\n        \"2025-01-08\",\n        \"2025-01-09\",\n        \"2025-01-10\",\n        \"2025-01-11\",\n        \"2025-01-12\",\n        \"2025-01-13\",\n        \"2025-01-14\",\n        \"2025-01-15\",\n        \"2025-01-16\",\n        \"2025-01-17\",\n    ]\n)\nactual_pts = [40, 36, 30, 36, 29, 29, 29, 22, 15, 9, 4, 0]\n\ndf_actual = pd.DataFrame({\"date\": sprint_dates, \"remaining\": actual_pts, \"series\": \"Actual Remaining\"})\ndf_ideal = pd.DataFrame(\n    {\"date\": pd.to_datetime([\"2025-01-06\", \"2025-01-17\"]), \"remaining\": [40, 0], \"series\": \"Ideal Burndown\"}\n)\nweekend_bands = pd.DataFrame(\n    {\"start\": pd.to_datetime([\"2025-01-11\"]), \"end\": pd.to_datetime([\"2025-01-13\"]), \"top\": [46.0], \"bottom\": [0.0]}\n)\nscope_df = pd.DataFrame({\"date\": pd.to_datetime([\"2025-01-09\"]), \"label\": [\"+8 pts\"]})\n\n# Shared color scale — actual (green first series) vs ideal (muted)\ncolor_scale = alt.Scale(domain=[\"Actual Remaining\", \"Ideal Burndown\"], range=[BRAND, INK_SOFT])\nseries_legend = alt.Legend(title=None, symbolType=\"stroke\", symbolStrokeWidth=3, labelFontSize=10)\n\ny_scale = alt.Scale(domain=[0, 46])\nx_axis = alt.Axis(format=\"%b %d\", labelAngle=-35, title=\"Sprint Date\")\ny_axis = alt.Axis(title=\"Story Points Remaining\")\n\n# Weekend shading using data coordinates — spans full y-range unobtrusively\nweekend_layer = (\n    alt.Chart(weekend_bands)\n    .mark_rect(opacity=0.08, color=INK_MUTED)\n    .encode(x=\"start:T\", x2=\"end:T\", y=\"top:Q\", y2=\"bottom:Q\")\n)\n\n# Ideal burndown — straight dashed diagonal reference line\nideal_layer = (\n    alt.Chart(df_ideal)\n    .mark_line(strokeDash=[10, 5], strokeWidth=2)\n    .encode(\n        x=alt.X(\"date:T\", axis=x_axis),\n        y=alt.Y(\"remaining:Q\", scale=y_scale, axis=y_axis),\n        color=alt.Color(\"series:N\", scale=color_scale, legend=series_legend),\n    )\n)\n\n# Actual remaining — step-after series (work done in discrete chunks)\nactual_layer = (\n    alt.Chart(df_actual)\n    .mark_line(interpolate=\"step-after\", strokeWidth=4)\n    .encode(\n        x=alt.X(\"date:T\", axis=x_axis),\n        y=alt.Y(\"remaining:Q\", scale=y_scale, axis=y_axis),\n        color=alt.Color(\"series:N\", scale=color_scale, legend=series_legend),\n        tooltip=[\n            alt.Tooltip(\"date:T\", title=\"Date\", format=\"%b %d\"),\n            alt.Tooltip(\"remaining:Q\", title=\"Pts Remaining\"),\n        ],\n    )\n)\n\n# Scope change: amber dashed vertical rule + label\nscope_rule = alt.Chart(scope_df).mark_rule(strokeDash=[6, 3], strokeWidth=2, color=ANYPLOT_AMBER).encode(x=\"date:T\")\nscope_label = (\n    alt.Chart(scope_df)\n    .mark_text(align=\"left\", dx=6, baseline=\"top\", color=ANYPLOT_AMBER, fontSize=11, fontWeight=\"bold\")\n    .encode(x=\"date:T\", y=alt.value(30), text=\"label:N\")\n)\n\n# Ahead/behind schedule annotations — makes the reading explicit per spec\n# Team is briefly ahead Jan 07–08 (actual < ideal), then behind after scope jump\nahead_df = pd.DataFrame({\"date\": pd.to_datetime([\"2025-01-08\"]), \"remaining\": [31.5], \"label\": [\"↓ Ahead\"]})\nahead_annotation = (\n    alt.Chart(ahead_df)\n    .mark_text(align=\"center\", dy=-4, fontSize=9, color=BRAND, fontStyle=\"italic\")\n    .encode(x=\"date:T\", y=alt.Y(\"remaining:Q\", scale=y_scale), text=\"label:N\")\n)\nbehind_df = pd.DataFrame({\"date\": pd.to_datetime([\"2025-01-14\"]), \"remaining\": [17.0], \"label\": [\"↑ Behind\"]})\nbehind_annotation = (\n    alt.Chart(behind_df)\n    .mark_text(align=\"left\", dx=4, fontSize=9, color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"date:T\", y=alt.Y(\"remaining:Q\", scale=y_scale), text=\"label:N\")\n)\n\n# Compose and configure\ntitle_str = \"burndown-sprint · python · altair · anyplot.ai\"\n\nchart = (\n    alt.layer(weekend_layer, ideal_layer, actual_layer, scope_rule, scope_label, ahead_annotation, behind_annotation)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(text=title_str, fontSize=16, color=INK, anchor=\"start\", offset=10),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=620, continuousHeight=320)\n    .configure_axisX(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        grid=False,\n        labelColor=INK_SOFT,\n        labelFontSize=10,\n        titleColor=INK,\n        titleFontSize=12,\n    )\n    .configure_axisY(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n        labelColor=INK_SOFT,\n        labelFontSize=10,\n        titleColor=INK,\n        titleFontSize=12,\n    )\n    .configure_title(color=INK, fontSize=16)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        labelFontSize=10,\n        orient=\"top-right\",\n        padding=8,\n    )\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 (vl-convert may undershoot)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML (interactive)\nchart.save(f\"plot-{THEME}.html\")\n"}