A sprint burndown chart tracks the amount of remaining work in an agile sprint over time, plotted as a step (or line) series that descends as tasks are completed. A straight ideal-burndown guideline runs diagonally from the total committed scope on day one down to zero on the final day, giving the team an at-a-glance reference for whether they are ahead of or behind schedule. It is one of the most widely used project-management visualizations in software development, surfacing scope creep, stalled progress, and end-of-sprint risk early enough to act on.

""" anyplot.ai
burndown-sprint: Agile Sprint Burndown Chart
Library: matplotlib 3.11.0 | Python 3.13.13
Quality: 89/100 | Created: 2026-06-14
"""
import sys
# Prevent this file from shadowing the installed matplotlib package when run
# from its own directory (sys.path[0] would otherwise resolve to this file).
sys.path.pop(0)
import os
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
# Imprint palette positions used
BRAND = "#009E73" # brand green — actual remaining (first series, always)
BLUE = "#4467A3" # blue — ideal burndown reference line
RED = "#AE3030" # matte red — scope change / behind-schedule indicator
# Data — 10-working-day sprint: Mon Jan 6 to Fri Jan 17 2025
# Committed scope: 40 story points; scope change (+10) occurs on Thu Jan 9
sprint_dates = pd.bdate_range("2025-01-06", "2025-01-17") # 10 business days
remaining = np.array([40, 38, 34, 44, 40, 36, 28, 20, 12, 4])
ideal = np.linspace(40, 0, len(sprint_dates))
scope_change_date = sprint_dates[3] # Jan 9: remaining jumps 34 → 44 (+10 scope)
# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)
# Weekend background band (Sat–Sun between the two working weeks)
ax.axvspan(pd.Timestamp("2025-01-11"), pd.Timestamp("2025-01-13"), color=INK, alpha=0.04, zorder=0)
ax.text(pd.Timestamp("2025-01-12"), 2, "wknd", fontsize=8, ha="center", va="bottom", color=INK_MUTED, rotation=90)
# Behind-schedule fill: region where actual remaining exceeds the ideal target
ax.fill_between(sprint_dates, remaining, ideal, where=(remaining > ideal), color=RED, alpha=0.07, step="post", zorder=1)
# Ideal burndown — straight diagonal reference from committed scope to zero
ax.plot(sprint_dates, ideal, color=BLUE, linewidth=1.8, linestyle="--", alpha=0.75, label="Ideal burndown", zorder=2)
# Actual remaining — step series (work completes in discrete daily chunks)
ax.step(sprint_dates, remaining, where="post", color=BRAND, linewidth=2.8, label="Actual remaining", zorder=3)
ax.scatter([sprint_dates[-1]], [remaining[-1]], color=BRAND, s=80, zorder=4, edgecolors=PAGE_BG, linewidth=1.0)
# Scope change: vertical dotted line + label above the spike
ax.axvline(scope_change_date, color=RED, linewidth=1.2, linestyle=":", alpha=0.85, zorder=4)
ax.text(
scope_change_date + pd.Timedelta(hours=8),
47.5,
"+10 pts scope",
fontsize=7,
color=RED,
ha="left",
va="bottom",
bbox={"facecolor": ELEVATED_BG, "edgecolor": RED, "alpha": 0.88, "linewidth": 0.7, "boxstyle": "round,pad=0.3"},
zorder=5,
)
# "Behind schedule" label at peak deviation — Jan 13 (Mon), midpoint between step and ideal
ax.text(
sprint_dates[5] + pd.Timedelta(hours=12),
(remaining[5] + ideal[5]) / 2,
"behind schedule",
fontsize=7,
color=RED,
alpha=0.55,
ha="center",
va="center",
rotation=90,
)
# Axis limits and labels
ax.set_xlim(sprint_dates[0] - pd.Timedelta(hours=12), sprint_dates[-1] + pd.Timedelta(hours=12))
ax.set_ylim(-1, 55)
ax.set_ylabel("Remaining Story Points", fontsize=10, color=INK)
ax.set_xlabel("Sprint Day · Jan 6 – 17 2025", fontsize=10, color=INK)
# X-axis ticks: Mon, Wed, Fri
ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=[0, 2, 4]))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%a %d"))
plt.setp(ax.get_xticklabels(), rotation=0, ha="center", fontsize=8, color=INK_SOFT)
ax.tick_params(axis="y", labelsize=8, labelcolor=INK_SOFT)
ax.tick_params(axis="x", colors=INK_SOFT)
# Grid and spines
ax.yaxis.grid(True, alpha=0.12, linewidth=0.7, color=INK)
ax.set_axisbelow(True)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for s in ("left", "bottom"):
ax.spines[s].set_color(INK_SOFT)
# Title
title = "burndown-sprint · python · matplotlib · anyplot.ai"
ax.set_title(title, fontsize=12, fontweight="medium", color=INK, pad=10)
# Legend
leg = ax.legend(fontsize=8, loc="upper right")
if leg:
leg.get_frame().set_facecolor(ELEVATED_BG)
leg.get_frame().set_edgecolor(INK_SOFT)
plt.setp(leg.get_texts(), color=INK_SOFT)
fig.subplots_adjust(left=0.10, right=0.97, top=0.92, bottom=0.12)
# Save
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)
plt.close()
Part of Agile Sprint Burndown Chart on anyplot.ai.