Basic Stream Graph — Seaborn

A streamgraph (also known as a stacked area chart with a centered baseline) displaying the composition of multiple categories over time with smooth, flowing curves. Unlike traditional stacked area charts, streamgraphs use a symmetric baseline centered around the x-axis, creating an organic, river-like appearance that emphasizes the overall shape and relative proportions of each category while minimizing the visual distortion of individual layers.

Basic Stream Graph rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
streamgraph-basic: Basic Stream Graph
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 86/100 | Updated: 2026-05-05
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from scipy.interpolate import make_interp_spline


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"

IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"]

sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.10,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Data — monthly streaming hours by music genre over 2 years
np.random.seed(42)

months = pd.date_range("2023-01", periods=24, freq="ME")
genres = ["Pop", "Rock", "Hip-Hop", "Electronic", "Classical", "Jazz"]

data = {}
for i, genre in enumerate(genres):
    base = [40, 35, 50, 30, 15, 12][i]
    trend = np.linspace(0, [10, -5, 15, 8, 2, 5][i], 24)
    seasonal = 5 * np.sin(np.linspace(0, 4 * np.pi, 24) + i)
    noise = np.random.randn(24) * 3
    data[genre] = np.maximum(base + trend + seasonal + noise, 5)

df = pd.DataFrame(data, index=months)

# Streamgraph: centered baseline
values = df.values
cumsum = np.cumsum(values, axis=1)
total = cumsum[:, -1]
baseline = -total / 2

lowers = np.column_stack([baseline + cumsum[:, i] - values[:, i] for i in range(len(genres))])
uppers = np.column_stack([baseline + cumsum[:, i] for i in range(len(genres))])

# Smooth spline interpolation for flowing curves
x_numeric = np.arange(len(months), dtype=float)
x_smooth = np.linspace(0, len(months) - 1, 400)

# Plot
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Store splines to reuse for trend lines and annotations
splines = []
for i in range(len(genres)):
    spl_lower = make_interp_spline(x_numeric, lowers[:, i], k=3)
    spl_upper = make_interp_spline(x_numeric, uppers[:, i], k=3)
    splines.append((spl_lower, spl_upper))
    ax.fill_between(
        x_smooth,
        spl_lower(x_smooth),
        spl_upper(x_smooth),
        label=genres[i],
        color=IMPRINT[i],
        alpha=0.85,
        edgecolor=PAGE_BG,
        linewidth=0.5,
    )

# Seaborn-native center-line trend highlights for Hip-Hop and Rock
# sns.lineplot adds a genuine seaborn plotting element over the streams
for gname, linestyle in [("Hip-Hop", (0, (6, 3))), ("Rock", (0, (6, 3)))]:
    gi = genres.index(gname)
    spl_lo, spl_up = splines[gi]
    center_vals = (spl_lo(x_smooth) + spl_up(x_smooth)) / 2
    center_df = pd.DataFrame({"x": x_smooth, "y": center_vals})
    sns.lineplot(
        data=center_df,
        x="x",
        y="y",
        ax=ax,
        color=IMPRINT[gi],
        linewidth=2.5,
        linestyle=linestyle,
        alpha=0.85,
        legend=False,
    )

# Data storytelling: annotate the two dominant narrative threads
hip_hop_idx = genres.index("Hip-Hop")
rock_idx = genres.index("Rock")

# Hip-Hop center near month 20 (growth is visible by then)
hh_x = 20
hh_center = (splines[hip_hop_idx][0](hh_x) + splines[hip_hop_idx][1](hh_x)) / 2
ax.annotate(
    "Hip-Hop\nrising ↑",
    xy=(hh_x, hh_center),
    xytext=(hh_x - 4, hh_center + 28),
    fontsize=15,
    fontweight="bold",
    color=IMPRINT[hip_hop_idx],
    arrowprops={"arrowstyle": "->", "color": INK_SOFT, "lw": 1.5},
)

# Rock center near month 18 (decline well established)
rk_x = 18
rk_center = (splines[rock_idx][0](rk_x) + splines[rock_idx][1](rk_x)) / 2
ax.annotate(
    "Rock\ndeclining ↓",
    xy=(rk_x, rk_center),
    xytext=(rk_x - 5, rk_center - 32),
    fontsize=15,
    fontweight="bold",
    color=IMPRINT[rock_idx],
    arrowprops={"arrowstyle": "->", "color": INK_SOFT, "lw": 1.5},
)

# Style
tick_positions = [0, 4, 8, 12, 16, 20, 23]
tick_labels = [months[i].strftime("%b '%y") for i in tick_positions]
ax.set_xticks(tick_positions)
ax.set_xticklabels(tick_labels, fontsize=16, color=INK_SOFT)

ax.set_xlim(0, len(months) - 1)
ax.set_yticks([])
ax.set_ylabel("")
ax.set_xlabel("Month", fontsize=20, color=INK)
ax.set_title("streamgraph-basic · seaborn · anyplot.ai", fontsize=24, fontweight="medium", color=INK)

ax.legend(
    loc="upper left",
    fontsize=16,
    title="Genre",
    title_fontsize=16,
    facecolor=ELEVATED_BG,
    edgecolor=INK_SOFT,
    framealpha=0.9,
)

sns.despine(ax=ax, left=True, bottom=False)
ax.spines["bottom"].set_color(INK_SOFT)

plt.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

Part of Basic Stream Graph on anyplot.ai.

Other implementations