Chris Parmer — home

Income Statement Sankey

revenue decomposition to net income

Example from the compendium of canonical charts

Income Statement Sankey — revenue decomposition to net income

Python Code

"""Income Statement Sankey — revenue decomposition to net income."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import plotly.graph_objects as go


# Nodes
# 0=Revenue, 1=COGS, 2=Gross Profit, 3=R&D, 4=S&M, 5=G&A, 6=Operating Income,
# 7=Interest/Other, 8=Tax, 9=Net Income
NODES = [
    "Revenue",          # 0
    "COGS",             # 1
    "Gross Profit",     # 2
    "R&D",              # 3
    "Sales & Mktg",     # 4
    "G&A",              # 5
    "Operating Income", # 6
    "Interest/Other",   # 7
    "Tax",              # 8
    "Net Income",       # 9
]

# ($M) — illustrative tech-company P&L
FLOWS = [
    (0, 1, 380),   # Revenue → COGS
    (0, 2, 620),   # Revenue → Gross Profit
    (2, 3, 180),   # Gross Profit → R&D
    (2, 4, 140),   # Gross Profit → Sales & Mktg
    (2, 5,  60),   # Gross Profit → G&A
    (2, 6, 240),   # Gross Profit → Operating Income
    (6, 7,  20),   # Operating Income → Interest/Other
    (6, 8,  50),   # Operating Income → Tax
    (6, 9, 170),   # Operating Income → Net Income
]

NODE_COLORS = [
    "#845EEE",  # Revenue
    "#DA5597",  # COGS
    "#52B3D0",  # Gross Profit
    "#E9A23B",  # R&D
    "#E9A23B",  # S&M
    "#E9A23B",  # G&A
    "#55B685",  # Operating Income
    "#DA5597",  # Interest
    "#DA5597",  # Tax
    "#55B685",  # Net Income
]


def generate():
    print("building Income Statement Sankey …")
    sources = [f[0] for f in FLOWS]
    targets = [f[1] for f in FLOWS]
    values  = [f[2] for f in FLOWS]

    fig = go.Figure(go.Sankey(
        node=dict(
            label=NODES,
            color=NODE_COLORS,
            pad=20, thickness=24,
            line=dict(color="rgba(0,0,0,0.08)", width=0.5),
        ),
        link=dict(
            source=sources,
            target=targets,
            value=values,
            color="rgba(150,150,150,0.2)",
        ),
    ))

    fig.update_layout(
        title=dict(text="Income Statement Flow ($M)", x=0.5),
        margin=dict(t=60, b=40, l=20, r=20),
        height=480,
        font=dict(size=11),
    )
    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly