Basic Stream Graph — Bokeh

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 Bokeh

Renders

Python source (Bokeh)

""" anyplot.ai
streamgraph-basic: Basic Stream Graph
Library: bokeh 3.9.2 | Python 3.13.14
Quality: 89/100 | Updated: 2026-08-05
"""

import os
import time
from pathlib import Path

import numpy as np
import pandas as pd
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource, HoverTool, Label, Legend, Range1d
from bokeh.plotting import figure
from scipy.interpolate import PchipInterpolator
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens
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 palette — first series always #009E73
COLORS = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"]

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

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

n_points = len(months)
base = np.linspace(0, 4 * np.pi, n_points)

raw = {
    "Pop": 45 + 18 * np.sin(base) + np.random.randn(n_points) * 3,
    "Rock": 38 + 12 * np.sin(base + 0.8) + np.random.randn(n_points) * 2.5,
    "Hip-Hop": 35 + 22 * np.sin(base + 1.6) + np.random.randn(n_points) * 4,
    "Electronic": 28 + 14 * np.sin(base + 2.4) + np.random.randn(n_points) * 2.5,
    "Jazz": 18 + 10 * np.sin(base + 3.2) + np.random.randn(n_points) * 2,
    "Classical": 14 + 6 * np.sin(base + 4.0) + np.random.randn(n_points) * 1.5,
}

for cat in categories:
    raw[cat] = np.maximum(raw[cat], 5)

df = pd.DataFrame(raw)

# Symmetric baseline — center the stack around zero
values = df[categories].values
total = values.sum(axis=1)
baseline_offset = total / 2

y_bottom = np.zeros_like(values)
y_top = np.zeros_like(values)
for i in range(len(categories)):
    if i == 0:
        y_bottom[:, i] = -baseline_offset
        y_top[:, i] = y_bottom[:, i] + values[:, i]
    else:
        y_bottom[:, i] = y_top[:, i - 1]
        y_top[:, i] = y_bottom[:, i] + values[:, i]

# Smooth interpolation for flowing curves.
# PCHIP (monotone cubic Hermite) is shape-preserving and does not overshoot at
# the series edges the way a high-degree polynomial fit (Runge phenomenon) does.
x_numeric = np.arange(n_points)
n_smooth = n_points * 10
x_smooth = np.linspace(0, n_points - 1, n_smooth)

months_smooth = pd.date_range(start=months.min(), end=months.max(), periods=n_smooth)
y_bottom_smooth = np.zeros((n_smooth, len(categories)))
y_top_smooth = np.zeros((n_smooth, len(categories)))

for i in range(len(categories)):
    y_bottom_smooth[:, i] = PchipInterpolator(x_numeric, y_bottom[:, i])(x_smooth)
    y_top_smooth[:, i] = PchipInterpolator(x_numeric, y_top[:, i])(x_smooth)

# Extra headroom above/below the widest point of the stack so the bands don't
# crowd the top/bottom plot edges.
max_disp = np.max(baseline_offset)
y_limit = max_disp * 1.3

# Plot
p = figure(
    width=3200,
    height=1800,
    title="streamgraph-basic · python · bokeh · anyplot.ai",
    x_axis_label="Month",
    y_axis_label="Streaming Hours (relative)",
    x_axis_type="datetime",
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=50,
)
p.y_range = Range1d(start=-y_limit, end=y_limit)

# Font sizes for 3200×1800 px canvas
p.title.text_font_size = "50pt"
p.title.text_color = INK
p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"

# Theme-adaptive chrome
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = None
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.10
p.ygrid.grid_line_alpha = 0.10

# Draw streamgraph patches
x_values = months_smooth.values
legend_items = []
hover_renderers = []

for i, cat in enumerate(categories):
    xs = np.concatenate([x_values, x_values[::-1]])
    ys = np.concatenate([y_top_smooth[:, i], y_bottom_smooth[:, i][::-1]])

    source = ColumnDataSource(data={"x": xs, "y": ys, "genre": [cat] * len(xs)})
    renderer = p.patch(
        x="x", y="y", source=source, fill_color=COLORS[i], fill_alpha=0.85, line_color=PAGE_BG, line_width=1
    )
    legend_items.append((cat, [renderer]))
    hover_renderers.append(renderer)

# HoverTool — shows genre name on hover
hover = HoverTool(renderers=hover_renderers, tooltips=[("Genre", "@genre")])
p.add_tools(hover)

# Legend outside the plot area
legend = Legend(items=legend_items, location="center")
legend.label_text_font_size = "34pt"
legend.label_text_color = INK_SOFT
legend.glyph_height = 44
legend.glyph_width = 44
legend.spacing = 15
legend.background_fill_color = ELEVATED_BG
legend.border_line_color = INK_SOFT
p.add_layout(legend, "right")

# Focal-point callout on the genre with the highest peak streaming month —
# gives the chart a storytelling anchor instead of leaving all six bands
# equally weighted.
peak_genre = "Pop"
peak_idx = int(np.argmax(raw[peak_genre]))
peak_x = months[peak_idx]
peak_y = (y_top[peak_idx, 0] + y_bottom[peak_idx, 0]) / 2

peak_marker_source = ColumnDataSource(data={"x": [peak_x], "y": [peak_y]})
p.scatter(x="x", y="y", source=peak_marker_source, size=22, fill_color=COLORS[0], line_color=INK, line_width=3)

peak_label = Label(
    x=peak_x,
    y=peak_y,
    x_offset=40,
    y_offset=70,
    text=f"{peak_genre} — peak streaming month",
    text_font_size="28pt",
    text_color=INK,
    background_fill_color=ELEVATED_BG,
    background_fill_alpha=0.95,
    border_line_color=INK_SOFT,
    border_line_width=1,
)
p.add_layout(peak_label)

# Save interactive HTML
output_file(f"plot-{THEME}.html")
save(p)

# Screenshot with headless Chrome (export_png unavailable in this environment)
W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)

driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/streamgraph-basic/bokeh/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "streamgraph-basic",
  "language": "python",
  "library": "bokeh",
  "page": "https://anyplot.ai/streamgraph-basic/python/bokeh",
  "hub": "https://anyplot.ai/streamgraph-basic",
  "code_json": "https://api.anyplot.ai/specs/streamgraph-basic/bokeh/code",
  "spec_json": "https://api.anyplot.ai/specs/streamgraph-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/bokeh/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/bokeh/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/bokeh/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/bokeh/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Stream Graph on anyplot.ai.

Other implementations