Skip to content

Commit 0193130

Browse files
feat(seaborn): implement streamgraph-basic (#5698)
## Implementation: `streamgraph-basic` - python/seaborn Implements the **python/seaborn** version of `streamgraph-basic`. **File:** `plots/streamgraph-basic/implementations/python/seaborn.py` **Parent Issue:** #856 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25355906940)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent d7f30db commit 0193130

2 files changed

Lines changed: 297 additions & 180 deletions

File tree

Lines changed: 122 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
streamgraph-basic: Basic Stream Graph
3-
Library: seaborn 0.13.2 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: seaborn 0.13.2 | Python 3.13.13
4+
Quality: 86/100 | Updated: 2026-05-05
55
"""
66

7+
import os
8+
79
import matplotlib.pyplot as plt
810
import numpy as np
911
import pandas as pd
1012
import seaborn as sns
11-
12-
13-
# Set seaborn theme for consistent styling
14-
sns.set_theme(style="whitegrid", context="talk", font_scale=1.2)
15-
16-
# Data - Monthly streaming hours by music genre over 2 years
13+
from scipy.interpolate import make_interp_spline
14+
15+
16+
THEME = os.getenv("ANYPLOT_THEME", "light")
17+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
21+
22+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9"]
23+
24+
sns.set_theme(
25+
style="ticks",
26+
rc={
27+
"figure.facecolor": PAGE_BG,
28+
"axes.facecolor": PAGE_BG,
29+
"axes.edgecolor": INK_SOFT,
30+
"axes.labelcolor": INK,
31+
"text.color": INK,
32+
"xtick.color": INK_SOFT,
33+
"ytick.color": INK_SOFT,
34+
"grid.color": INK,
35+
"grid.alpha": 0.10,
36+
"legend.facecolor": ELEVATED_BG,
37+
"legend.edgecolor": INK_SOFT,
38+
},
39+
)
40+
41+
# Data — monthly streaming hours by music genre over 2 years
1742
np.random.seed(42)
1843

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

22-
# Generate realistic streaming data with seasonal patterns
2347
data = {}
2448
for i, genre in enumerate(genres):
2549
base = [40, 35, 50, 30, 15, 12][i]
@@ -30,65 +54,113 @@
3054

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

33-
# Create long-form data for seaborn
34-
df_long = df.reset_index().melt(id_vars="index", var_name="Genre", value_name="Hours")
35-
df_long.rename(columns={"index": "Month"}, inplace=True)
36-
37-
# Pivot to wide format for stackplot calculation
38-
df_pivot = df_long.pivot(index="Month", columns="Genre", values="Hours")
39-
df_pivot = df_pivot[genres] # Ensure consistent order
40-
41-
# Create stacked values for streamgraph (centered baseline)
42-
values = df_pivot.values
57+
# Streamgraph: centered baseline
58+
values = df.values
4359
cumsum = np.cumsum(values, axis=1)
4460
total = cumsum[:, -1]
45-
baseline = -total / 2 # Center around x-axis
61+
baseline = -total / 2
4662

47-
# Create upper and lower bounds for each genre
4863
lowers = np.column_stack([baseline + cumsum[:, i] - values[:, i] for i in range(len(genres))])
4964
uppers = np.column_stack([baseline + cumsum[:, i] for i in range(len(genres))])
5065

51-
# Create figure using seaborn-styled matplotlib
52-
fig, ax = plt.subplots(figsize=(16, 9))
66+
# Smooth spline interpolation for flowing curves
67+
x_numeric = np.arange(len(months), dtype=float)
68+
x_smooth = np.linspace(0, len(months) - 1, 400)
5369

54-
# Use seaborn color palette - Python colors first, then colorblind-safe
55-
palette = ["#306998", "#FFD43B"] + sns.color_palette("colorblind", n_colors=4).as_hex()
70+
# Plot
71+
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
72+
ax.set_facecolor(PAGE_BG)
5673

57-
# Fill between for each genre layer
74+
# Store splines to reuse for trend lines and annotations
75+
splines = []
5876
for i in range(len(genres)):
77+
spl_lower = make_interp_spline(x_numeric, lowers[:, i], k=3)
78+
spl_upper = make_interp_spline(x_numeric, uppers[:, i], k=3)
79+
splines.append((spl_lower, spl_upper))
5980
ax.fill_between(
60-
df_pivot.index,
61-
lowers[:, i],
62-
uppers[:, i],
81+
x_smooth,
82+
spl_lower(x_smooth),
83+
spl_upper(x_smooth),
6384
label=genres[i],
64-
color=palette[i],
85+
color=OKABE_ITO[i],
6586
alpha=0.85,
66-
edgecolor="white",
87+
edgecolor=PAGE_BG,
6788
linewidth=0.5,
6889
)
6990

70-
# Styling with seaborn aesthetics
71-
ax.set_xlabel("Month", fontsize=20)
72-
ax.set_title("streamgraph-basic · seaborn · pyplots.ai", fontsize=24)
73-
ax.tick_params(axis="both", labelsize=16)
91+
# Seaborn-native center-line trend highlights for Hip-Hop and Rock
92+
# sns.lineplot adds a genuine seaborn plotting element over the streams
93+
for gname, linestyle in [("Hip-Hop", (0, (6, 3))), ("Rock", (0, (6, 3)))]:
94+
gi = genres.index(gname)
95+
spl_lo, spl_up = splines[gi]
96+
center_vals = (spl_lo(x_smooth) + spl_up(x_smooth)) / 2
97+
center_df = pd.DataFrame({"x": x_smooth, "y": center_vals})
98+
sns.lineplot(
99+
data=center_df,
100+
x="x",
101+
y="y",
102+
ax=ax,
103+
color=OKABE_ITO[gi],
104+
linewidth=2.5,
105+
linestyle=linestyle,
106+
alpha=0.85,
107+
legend=False,
108+
)
74109

75-
# Remove y-axis ticks (streamgraph focuses on relative proportions)
110+
# Data storytelling: annotate the two dominant narrative threads
111+
hip_hop_idx = genres.index("Hip-Hop")
112+
rock_idx = genres.index("Rock")
113+
114+
# Hip-Hop center near month 20 (growth is visible by then)
115+
hh_x = 20
116+
hh_center = (splines[hip_hop_idx][0](hh_x) + splines[hip_hop_idx][1](hh_x)) / 2
117+
ax.annotate(
118+
"Hip-Hop\nrising ↑",
119+
xy=(hh_x, hh_center),
120+
xytext=(hh_x - 4, hh_center + 28),
121+
fontsize=15,
122+
fontweight="bold",
123+
color=OKABE_ITO[hip_hop_idx],
124+
arrowprops={"arrowstyle": "->", "color": INK_SOFT, "lw": 1.5},
125+
)
126+
127+
# Rock center near month 18 (decline well established)
128+
rk_x = 18
129+
rk_center = (splines[rock_idx][0](rk_x) + splines[rock_idx][1](rk_x)) / 2
130+
ax.annotate(
131+
"Rock\ndeclining ↓",
132+
xy=(rk_x, rk_center),
133+
xytext=(rk_x - 5, rk_center - 32),
134+
fontsize=15,
135+
fontweight="bold",
136+
color=OKABE_ITO[rock_idx],
137+
arrowprops={"arrowstyle": "->", "color": INK_SOFT, "lw": 1.5},
138+
)
139+
140+
# Style
141+
tick_positions = [0, 4, 8, 12, 16, 20, 23]
142+
tick_labels = [months[i].strftime("%b '%y") for i in tick_positions]
143+
ax.set_xticks(tick_positions)
144+
ax.set_xticklabels(tick_labels, fontsize=16, color=INK_SOFT)
145+
146+
ax.set_xlim(0, len(months) - 1)
76147
ax.set_yticks([])
77148
ax.set_ylabel("")
149+
ax.set_xlabel("Month", fontsize=20, color=INK)
150+
ax.set_title("streamgraph-basic · seaborn · anyplot.ai", fontsize=24, fontweight="medium", color=INK)
151+
152+
ax.legend(
153+
loc="upper left",
154+
fontsize=16,
155+
title="Genre",
156+
title_fontsize=16,
157+
facecolor=ELEVATED_BG,
158+
edgecolor=INK_SOFT,
159+
framealpha=0.9,
160+
)
78161

79-
# Format x-axis dates
80-
ax.set_xlim(df_pivot.index[0], df_pivot.index[-1])
81-
fig.autofmt_xdate(rotation=45)
82-
83-
# Legend using seaborn styling
84-
ax.legend(loc="upper left", fontsize=14, framealpha=0.9, title="Genre", title_fontsize=16)
85-
86-
# Use seaborn's despine for cleaner look
87162
sns.despine(ax=ax, left=True, bottom=False)
88-
89-
# Subtle grid on x-axis only
90-
ax.grid(True, axis="x", alpha=0.3, linestyle="--")
91-
ax.set_axisbelow(True)
163+
ax.spines["bottom"].set_color(INK_SOFT)
92164

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

0 commit comments

Comments
 (0)