Chris Parmer — home

Cumulative Flow Diagram

GitHub issues stacked area by state over time

Example from the compendium of canonical charts

Cumulative Flow Diagram — GitHub issues stacked area by state over time

Python Code

"""Cumulative Flow Diagram — GitHub issues stacked area by state over time."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
import requests, warnings, time


REPO = "plotly/plotly.js"
ISSUES_URL = f"https://api.github.com/repos/{REPO}/issues"


def fetch_issues():
    warnings.filterwarnings("ignore")
    requests.packages.urllib3.disable_warnings()

    headers = {"Accept": "application/vnd.github+json",
               "X-GitHub-Api-Version": "2022-11-28"}

    issues = []
    page = 1
    while len(issues) < 500:
        r = requests.get(
            ISSUES_URL,
            params={"state": "all", "per_page": 100, "page": page, "filter": "all"},
            headers=headers,
            verify=False,
            timeout=30,
        )
        if r.status_code != 200:
            print(f"  page {page}: status={r.status_code}")
            break

        batch = r.json()
        if not batch:
            break

        # Filter out pull requests
        issues.extend([i for i in batch if "pull_request" not in i])
        print(f"  page {page}: +{len(batch)} → {len(issues)} issues so far")
        page += 1
        time.sleep(0.3)   # gentle rate limiting

    return issues


def generate():
    print(f"fetching issues for {REPO} …")
    try:
        issues = fetch_issues()
        if not issues:
            raise ValueError("No issues returned")

        rows = []
        for iss in issues:
            rows.append({
                "created": pd.to_datetime(iss["created_at"]).date(),
                "closed":  pd.to_datetime(iss["closed_at"]).date() if iss["closed_at"] else None,
                "state":   iss["state"],
            })
        df = pd.DataFrame(rows)
        source = REPO

    except Exception as e:
        print(f"  GitHub issues failed ({e}); using synthetic CFD data")
        rng = np.random.default_rng(30)
        dates = pd.date_range("2022-01-01", periods=365, freq="D")
        rows = []
        for i in range(600):
            created = dates[rng.integers(0, 300)]
            duration = rng.integers(1, 90)
            closed = created + pd.Timedelta(days=int(duration))
            if closed > dates[-1]:
                closed = None
            rows.append({"created": created.date(), "closed": closed, "state": "closed" if closed else "open"})
        df = pd.DataFrame(rows)
        source = "Synthetic"

    # Build weekly cumulative counts: open, closed
    all_dates = pd.date_range(
        start=pd.to_datetime(df["created"].min()),
        end=pd.Timestamp.today().date(),
        freq="W-MON",
    )

    weekly = []
    for d in all_dates:
        d_date = d.date()
        opened = (df["created"] <= d_date).sum()
        closed = ((df["closed"].notna()) & (pd.to_datetime(df["closed"]) <= d)).sum()
        open_now = opened - closed
        weekly.append({"date": d, "opened": int(opened), "closed": int(closed), "open": int(open_now)})

    wdf = pd.DataFrame(weekly)

    fig = go.Figure()

    # Bottom band: Closed — green fill
    fig.add_trace(go.Scatter(
        x=wdf["date"], y=wdf["closed"],
        mode="lines",
        name="Closed",
        line=dict(color=GREEN, width=2),
        fill="tozeroy",
        fillcolor="rgba(85,182,133,0.35)",
        hovertemplate="%{x|%b %Y}<br>Closed: %{y}<extra></extra>",
    ))

    # Upper band: Open backlog fills from closed up to total opened — violet fill
    fig.add_trace(go.Scatter(
        x=wdf["date"], y=wdf["opened"],
        mode="lines",
        name="Total opened",
        line=dict(color=VIOLET, width=2),
        fill="tonexty",
        fillcolor="rgba(132,94,238,0.30)",
        hovertemplate="%{x|%b %Y}<br>Total opened: %{y}<extra></extra>",
    ))

    fig.update_layout(
        title=dict(text=f"Cumulative Flow Diagram — {source} Issues", x=0.5),
        xaxis=dict(title="Week"),
        yaxis=dict(title="Issue count"),
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=60, b=50, l=70, r=40),
        height=460,
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly