{"spec_id":"burndown-sprint","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nburndown-sprint: Agile Sprint Burndown Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Created: 2026-06-14\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Remove script directory from sys.path to avoid self-import (file is named pygal.py)\n_here = os.path.dirname(os.path.abspath(__file__))\nif _here in sys.path:\n    sys.path.remove(_here)\n\nimport cairosvg\nimport pygal\nfrom pygal.style import Style\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\"\nWEEKEND_FILL = INK  # subtle non-working-day band\nSCOPE_COLOR = \"#C475FD\"  # Imprint violet — scope-change annotation\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nANYPLOT_NEUTRAL = INK  # reference lines / baseline (theme-adaptive)\n\ntitle = \"burndown-sprint · python · pygal · anyplot.ai\"\n\n# Data — 2-week sprint: 10 working days + 1 Sat/Sun weekend block\nx_labels = [\"Start\", \"Mon\", \"Tue\", \"Wed*\", \"Thu\", \"Fri\", \"Sat\", \"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\"]\n# Ideal burndown: 4 pts/working day from 40 → 0; flat on Sat and Sun\nideal = [40, 36, 32, 28, 24, 20, 20, 20, 16, 12, 8, 4, 0]\n# Actual remaining: scope change on Wed* (day 3) pushes line above ideal mid-sprint\nactual = [40, 36, 30, 32, 27, 22, 22, 22, 16, 10, 5, 2, 0]\n\n# Overlay landmark indices in x_labels\nWEEKEND_START_IDX = 6  # Sat\nWEEKEND_END_IDX = 7  # Sun\nSCOPE_CHANGE_IDX = 3  # Wed*\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[0], ANYPLOT_NEUTRAL),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=4,\n)\n\nchart = pygal.Line(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Sprint Day  (* Wed = scope +8 pts  |  Sat–Sun = weekend)\",\n    y_title=\"Remaining Story Points\",\n    show_x_guides=False,\n    show_y_guides=True,\n    show_dots=True,\n    dots_size=8,\n    stroke=True,\n    x_label_rotation=0,\n)\n\nchart.x_labels = x_labels\nchart.add(\"Actual\", actual, stroke_style={\"width\": 5})\nchart.add(\"Ideal\", ideal, stroke_style={\"width\": 3, \"dasharray\": \"8, 6\"})\n\n# Render to SVG then post-process: inject weekend band + scope-change marker\nsvg_str = chart.render().decode(\"utf-8\")\n\n# Extract x-coordinates from data-point circle elements (plot-group local space)\ncx_vals = sorted(\n    {int(round(float(m.group(1)))) for m in re.finditer(r\"<circle\\b[^>]+\\bcx=[\\\"']([^\\\"']+)[\\\"']\", svg_str)}\n)  # 13 unique positions, one per x_label\n\n# Estimate plot area height from maximum cy of data circles\ncy_vals = [float(m.group(1)) for m in re.finditer(r\"<circle\\b[^>]+\\bcy=[\\\"']([^\\\"']+)[\\\"']\", svg_str)]\nplot_height = int(max(cy_vals) + 80) if cy_vals else 1380\n\n# Build SVG overlay elements (injected before the first data series group)\noverlays = []\nif len(cx_vals) >= 13:\n    step = cx_vals[1] - cx_vals[0]\n\n    # Weekend shading band: covers from midpoint before Sat to midpoint after Sun\n    wx_left = cx_vals[WEEKEND_START_IDX] - step // 2\n    wx_right = cx_vals[WEEKEND_END_IDX] + step // 2\n    overlays.append(\n        f'<rect x=\"{wx_left}\" y=\"0\" width=\"{wx_right - wx_left}\" '\n        f'height=\"{plot_height}\" fill=\"{WEEKEND_FILL}\" fill-opacity=\"0.07\" stroke=\"none\"/>'\n    )\n\n    # Scope-change vertical dashed marker at Wed* (index 3)\n    sx = cx_vals[SCOPE_CHANGE_IDX]\n    overlays.append(\n        f'<line x1=\"{sx}\" y1=\"0\" x2=\"{sx}\" y2=\"{plot_height}\" '\n        f'stroke=\"{SCOPE_COLOR}\" stroke-width=\"4\" stroke-dasharray=\"14,7\" opacity=\"0.85\"/>'\n    )\n    # Inline annotation label near the top of the marker line\n    overlays.append(\n        f'<text x=\"{sx + 14}\" y=\"54\" fill=\"{SCOPE_COLOR}\" '\n        f'font-family=\"sans-serif\" font-size=\"38\" font-weight=\"600\" opacity=\"0.9\">'\n        f\"Scope +8 pts</text>\"\n    )\n\nif overlays:\n    # Insert inside the plot group, immediately before the first data series\n    for marker in ('<g class=\"series serie-0', '<g class=\"series '):\n        pos = svg_str.find(marker)\n        if pos >= 0:\n            svg_str = svg_str[:pos] + \"\\n\".join(overlays) + \"\\n\" + svg_str[pos:]\n            break\n\noutput_bytes = svg_str.encode(\"utf-8\")\ncairosvg.svg2png(bytestring=output_bytes, write_to=f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(output_bytes)\n"}