Chris Parmer — home

Sprint burn-down chart

Agile — story points remaining vs. ideal

Example from the compendium of canonical charts

Agile — Sprint burn-down chart (story points remaining vs. ideal)

Python Code

"""Agile — Sprint burn-down chart (story points remaining vs. ideal)."""


import numpy as np
import plotly.graph_objects as go


def generate():
    print("building sprint burn-down chart …")

    rng = np.random.default_rng(133)

    sprint_days = 10
    total_points = 80

    # Ideal burn: linear from total_points to 0
    days = np.arange(0, sprint_days + 1)
    ideal = total_points - total_points * days / sprint_days

    # Actual burn: realistic — slow start, accelerates, then levels off
    actual = [total_points]
    remaining = float(total_points)
    for d in range(1, sprint_days + 1):
        # Burn rate increases in middle of sprint
        if d <= 2:
            burn = rng.uniform(3, 6)
        elif d <= 7:
            burn = rng.uniform(8, 14)
        else:
            burn = rng.uniform(4, 8)
        remaining = max(0, remaining - burn)
        actual.append(remaining)

    actual = np.array(actual)

    # Add scope change on day 4 (new stories added)
    scope_day = 4
    scope_added = 12
    scope_remaining = actual.copy()
    scope_remaining[scope_day:] += scope_added

    fig = go.Figure()

    # Ideal line
    fig.add_trace(go.Scatter(
        x=days, y=ideal,
        mode="lines",
        name="Ideal",
        line=dict(color=MUTED, width=2, dash="dash"),
        hovertemplate="Day %{x}: %{y:.0f} pts ideal<extra></extra>",
    ))

    # Scope change area
    fig.add_trace(go.Scatter(
        x=days[scope_day:], y=scope_remaining[scope_day:],
        mode="lines",
        name="After scope change",
        line=dict(color=TEAL, width=2, dash="dot"),
        hovertemplate="Day %{x}: %{y:.0f} pts (post-scope)<extra></extra>",
    ))

    # Actual remaining
    fig.add_trace(go.Scatter(
        x=days, y=actual,
        mode="lines+markers",
        name="Actual remaining",
        line=dict(color=VIOLET, width=2.5),
        marker=dict(size=8, color=VIOLET, line=dict(color="white", width=1.5)),
        hovertemplate="Day %{x}: %{y:.0f} pts remaining<extra></extra>",
    ))

    # Scope change annotation
    fig.add_annotation(
        x=scope_day, y=actual[scope_day] + scope_added / 2,
        text=f"+{scope_added} pts added",
        showarrow=True, arrowhead=2,
        ax=50, ay=0,
        font=dict(size=11, color=TEAL),
        arrowcolor=TEAL,
        bgcolor="rgba(255,255,255,0.85)",
        bordercolor=TEAL,
    )

    # Sprint completion marker
    done_pts = int(total_points - actual[-1])
    fig.add_annotation(
        x=sprint_days, y=actual[-1],
        text=f"{done_pts} pts done",
        showarrow=True, arrowhead=2,
        ax=-50, ay=-30,
        font=dict(size=11, color=GREEN),
        arrowcolor=GREEN,
        bgcolor="rgba(255,255,255,0.85)",
        bordercolor=GREEN,
    )

    fig.update_layout(
        xaxis=dict(
            title="Sprint Day",
            tickvals=list(range(sprint_days + 1)),
            range=[-0.3, sprint_days + 0.5],
            showgrid=True,
        ),
        yaxis=dict(
            title="Story Points Remaining",
            range=[-5, total_points + scope_added + 5],
        ),
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=50, b=50, l=70, r=40),
        height=420,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly