Cumulative Flow Diagram
GitHub issues stacked area by state over time
Example from the compendium of canonical charts
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