Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 59 additions & 40 deletions plots/streamgraph-basic/implementations/python/letsplot.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,100 @@
""" pyplots.ai
""" anyplot.ai
streamgraph-basic: Basic Stream Graph
Library: letsplot 4.8.2 | Python 3.13.11
Quality: 91/100 | Created: 2025-12-23
Library: letsplot 4.9.0 | Python 3.13.13
Quality: 87/100 | Updated: 2026-05-06
"""

import os

import numpy as np
import pandas as pd
from lets_plot import * # noqa: F403
from lets_plot.export import ggsave as export_ggsave
from lets_plot.export import ggsave
from scipy.interpolate import make_interp_spline


LetsPlot.setup_html() # noqa: F405

# Data - monthly streaming hours by music genre over two years
# 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"

# Okabe-Ito palette — first series always #009E73
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]

# Data — monthly streaming hours by music genre over two years
np.random.seed(42)
n_months = 24
genres = ["Pop", "Rock", "Hip-Hop", "Electronic", "Jazz"]
n_genres = len(genres)

# Generate smooth trends for each genre with distinct patterns
raw_values = {}
months_orig = np.arange(n_months, dtype=float)
for i, genre in enumerate(genres):
base = 100 + 50 * np.sin(np.linspace(0, 4 * np.pi, n_months) + i * 0.7)
trend = np.linspace(0, 25, n_months) * (1 if i % 2 == 0 else -0.6)
noise = np.random.randn(n_months) * 8
raw_values[genre] = np.clip(base + trend + noise, 25, None)

# Compute streamgraph positions (centered around baseline)
values_matrix = np.array([raw_values[g] for g in genres])
total_per_month = values_matrix.sum(axis=0)
baseline_offset = -total_per_month / 2
# Smooth each series with a cubic spline for flowing curves
n_interp = n_months * 8
months_smooth = np.linspace(0, n_months - 1, n_interp)
values_smooth = {}
for genre in genres:
spline = make_interp_spline(months_orig, raw_values[genre], k=3)
values_smooth[genre] = np.clip(spline(months_smooth), 10, None)

# Compute streamgraph positions (symmetric baseline around zero)
values_matrix = np.array([values_smooth[g] for g in genres])
total_per_point = values_matrix.sum(axis=0)
baseline_offset = -total_per_point / 2

# Build dataframe with ymin/ymax for ribbon geometry
data = []
for month_idx in range(n_months):
cumulative = baseline_offset[month_idx]
for t_idx, t in enumerate(months_smooth):
cumulative = baseline_offset[t_idx]
for genre_idx, genre in enumerate(genres):
ymin = cumulative
ymax = cumulative + values_matrix[genre_idx, month_idx]
data.append({"month": month_idx, "genre": genre, "ymin": ymin, "ymax": ymax})
ymax = cumulative + values_matrix[genre_idx, t_idx]
data.append({"month": t, "genre": genre, "ymin": ymin, "ymax": ymax})
cumulative = ymax

df = pd.DataFrame(data)

# Create streamgraph using geom_ribbon for precise ymin/ymax control
# Plot
anyplot_theme = theme( # noqa: F405
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), # noqa: F405
panel_background=element_rect(fill=PAGE_BG), # noqa: F405
panel_grid_major_x=element_line(color=INK_SOFT, size=0.3, linetype="dashed"), # noqa: F405
panel_grid_major_y=element_blank(), # noqa: F405
panel_grid_minor=element_blank(), # noqa: F405
axis_title=element_text(color=INK, size=20), # noqa: F405
axis_text=element_text(color=INK_SOFT, size=16), # noqa: F405
axis_text_y=element_blank(), # noqa: F405
axis_ticks_y=element_blank(), # noqa: F405
axis_line=element_line(color=INK_SOFT), # noqa: F405
plot_title=element_text(color=INK, size=24), # noqa: F405
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), # noqa: F405
legend_text=element_text(color=INK_SOFT, size=16), # noqa: F405
legend_title=element_text(color=INK, size=18), # noqa: F405
)

plot = (
ggplot(df, aes(x="month", fill="genre")) # noqa: F405
+ geom_ribbon(aes(ymin="ymin", ymax="ymax"), alpha=0.9) # noqa: F405
+ scale_fill_manual( # noqa: F405
values=["#306998", "#FFD43B", "#38BDF8", "#A78BFA", "#FB923C"]
)
+ scale_fill_manual(values=OKABE_ITO) # noqa: F405
+ scale_x_continuous( # noqa: F405
breaks=[0, 6, 12, 18, 23], labels=["Jan '23", "Jul '23", "Jan '24", "Jul '24", "Dec '24"]
)
+ labs( # noqa: F405
x="Month", y="Streaming Hours", fill="Genre", title="streamgraph-basic · lets-plot · pyplots.ai"
x="Month", y="", fill="Genre", title="streamgraph-basic · letsplot · anyplot.ai"
)
+ ggsize(1600, 900) # noqa: F405
+ theme_minimal() # noqa: F405
+ theme( # noqa: F405
axis_text=element_text(size=16), # noqa: F405
axis_title=element_text(size=20), # noqa: F405
plot_title=element_text(size=24), # noqa: F405
legend_text=element_text(size=16), # noqa: F405
legend_title=element_text(size=18), # noqa: F405
axis_text_y=element_blank(), # noqa: F405
axis_ticks_y=element_blank(), # noqa: F405
panel_grid_major_y=element_blank(), # noqa: F405
panel_grid_minor_y=element_blank(), # noqa: F405
panel_grid_major_x=element_line( # noqa: F405
color="#CCCCCC", size=0.5, linetype="dashed"
),
)
+ anyplot_theme
)

# Save PNG (scale 3x to get 4800 x 2700 px)
export_ggsave(plot, filename="plot.png", path=".", scale=3)

# Save HTML for interactive version
export_ggsave(plot, filename="plot.html", path=".")
# Save
ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=3)
ggsave(plot, filename=f"plot-{THEME}.html", path=".")
Loading
Loading