|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | streamgraph-basic: Basic Stream Graph |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: plotly 6.7.0 | Python 3.13.13 |
| 4 | +Quality: 88/100 | Updated: 2026-05-05 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import sys |
| 8 | + |
| 9 | + |
| 10 | +sys.path.pop(0) |
| 11 | + |
| 12 | +import os |
| 13 | + |
7 | 14 | import numpy as np |
8 | 15 | import pandas as pd |
9 | 16 | import plotly.graph_objects as go |
10 | 17 |
|
11 | 18 |
|
| 19 | +# Theme tokens |
| 20 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 21 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 22 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 23 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 24 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 25 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 26 | + |
| 27 | +# Okabe-Ito palette — first series always #009E73 |
| 28 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9"] |
| 29 | + |
12 | 30 | # Data - Monthly streaming hours by music genre over 2 years |
13 | 31 | np.random.seed(42) |
14 | 32 | months = pd.date_range(start="2022-01-01", periods=24, freq="ME") |
15 | 33 | genres = ["Pop", "Rock", "Hip-Hop", "Electronic", "Jazz", "Classical"] |
16 | 34 |
|
17 | | -# Generate smooth, realistic streaming data |
18 | 35 | base_values = {"Pop": 45, "Rock": 35, "Hip-Hop": 40, "Electronic": 25, "Jazz": 15, "Classical": 12} |
19 | 36 | data = {} |
20 | 37 | for genre in genres: |
21 | | - # Create smooth trends with seasonal variation |
22 | 38 | trend = np.cumsum(np.random.randn(24) * 2) |
23 | 39 | seasonal = 5 * np.sin(np.linspace(0, 4 * np.pi, 24)) |
24 | 40 | noise = np.random.randn(24) * 3 |
25 | 41 | values = base_values[genre] + trend + seasonal + noise |
26 | | - values = np.maximum(values, 5) # Ensure positive values |
| 42 | + values = np.maximum(values, 5) |
27 | 43 | data[genre] = values |
28 | 44 |
|
29 | 45 | df = pd.DataFrame(data, index=months) |
| 46 | +month_labels = months.strftime("%Y-%m").tolist() |
30 | 47 |
|
31 | 48 | # Calculate streamgraph layout (centered baseline) |
32 | 49 | values_array = df.values.T # Shape: (n_genres, n_time_points) |
33 | 50 | n_genres, n_time = values_array.shape |
34 | 51 |
|
35 | | -# Calculate cumulative sums for stacking |
36 | 52 | cumsum = np.vstack([np.zeros(n_time), np.cumsum(values_array, axis=0)]) |
37 | | - |
38 | | -# Center the baseline symmetrically around the x-axis |
39 | 53 | total = cumsum[-1] |
40 | 54 | offset = total / 2 |
41 | 55 |
|
42 | | -# Colors - Python Blue and Yellow first, then colorblind-safe palette |
43 | | -colors = ["#306998", "#FFD43B", "#E24A33", "#8EBA42", "#988ED5", "#348ABD"] |
44 | | - |
45 | | -# Create figure |
| 56 | +# Plot |
46 | 57 | fig = go.Figure() |
47 | 58 |
|
48 | | -# Add each genre as a filled area with smooth spline curves |
49 | 59 | for i, genre in enumerate(genres): |
50 | 60 | y_lower = cumsum[i] - offset |
51 | 61 | y_upper = cumsum[i + 1] - offset |
52 | 62 |
|
53 | | - # Create coordinates for fill (forward then backward) |
54 | | - x_fill = list(months) + list(months)[::-1] |
| 63 | + x_fill = month_labels + month_labels[::-1] |
55 | 64 | y_fill = list(y_upper) + list(y_lower)[::-1] |
56 | 65 |
|
| 66 | + # Dominant stream (Pop) at full opacity; others slightly dimmed for visual hierarchy |
| 67 | + opacity = 1.0 if i == 0 else 0.80 |
| 68 | + |
57 | 69 | fig.add_trace( |
58 | 70 | go.Scatter( |
59 | 71 | x=x_fill, |
60 | 72 | y=y_fill, |
61 | 73 | fill="toself", |
62 | | - fillcolor=colors[i], |
63 | | - line={"color": colors[i], "width": 0.5, "shape": "spline", "smoothing": 1.0}, |
| 74 | + fillcolor=OKABE_ITO[i], |
| 75 | + opacity=opacity, |
| 76 | + line={"color": OKABE_ITO[i], "width": 0.5, "shape": "spline", "smoothing": 1.0}, |
64 | 77 | name=genre, |
65 | 78 | mode="none", |
66 | 79 | hoverinfo="name+x", |
67 | 80 | hoveron="fills", |
68 | 81 | ) |
69 | 82 | ) |
70 | 83 |
|
71 | | -# Update layout for 4800x2700 px |
| 84 | +subtitle = f"<span style='font-size:18px;color:{INK_SOFT}'>Monthly streaming hours by music genre, 2022–2023</span>" |
| 85 | + |
72 | 86 | fig.update_layout( |
73 | | - title={"text": "streamgraph-basic · plotly · pyplots.ai", "font": {"size": 36}, "x": 0.5, "xanchor": "center"}, |
| 87 | + title={ |
| 88 | + "text": f"streamgraph-basic · plotly · anyplot.ai<br>{subtitle}", |
| 89 | + "font": {"size": 28, "color": INK}, |
| 90 | + "x": 0.5, |
| 91 | + "xanchor": "center", |
| 92 | + }, |
74 | 93 | xaxis={ |
75 | | - "title": {"text": "Month", "font": {"size": 28}}, |
76 | | - "tickfont": {"size": 22}, |
77 | | - "showgrid": True, |
78 | | - "gridcolor": "rgba(128,128,128,0.2)", |
79 | | - "gridwidth": 1, |
| 94 | + "title": {"text": "Month", "font": {"size": 22, "color": INK}}, |
| 95 | + "tickfont": {"size": 18, "color": INK_SOFT}, |
| 96 | + "showgrid": False, |
| 97 | + "showline": True, |
| 98 | + "linecolor": INK_SOFT, |
| 99 | + "mirror": False, # bottom spine only — no top spine |
| 100 | + "zeroline": False, |
80 | 101 | }, |
81 | 102 | yaxis={ |
82 | | - "title": {"text": "Streaming Hours (Millions)", "font": {"size": 28}}, |
83 | | - "tickfont": {"size": 22}, |
| 103 | + "showticklabels": False, # hide confusing negative offset values |
84 | 104 | "showgrid": True, |
85 | | - "gridcolor": "rgba(128,128,128,0.2)", |
| 105 | + "gridcolor": GRID, |
86 | 106 | "gridwidth": 1, |
87 | 107 | "zeroline": True, |
88 | | - "zerolinecolor": "rgba(128,128,128,0.3)", |
| 108 | + "zerolinecolor": INK_SOFT, |
89 | 109 | "zerolinewidth": 1, |
| 110 | + "showline": False, # no left spine (y labels hidden anyway) |
| 111 | + "mirror": False, |
| 112 | + }, |
| 113 | + legend={ |
| 114 | + "font": {"size": 18, "color": INK_SOFT}, |
| 115 | + "bgcolor": ELEVATED_BG, |
| 116 | + "bordercolor": INK_SOFT, |
| 117 | + "borderwidth": 1, |
| 118 | + "orientation": "h", |
| 119 | + "yanchor": "top", |
| 120 | + "y": -0.12, |
| 121 | + "xanchor": "center", |
| 122 | + "x": 0.5, |
90 | 123 | }, |
91 | | - legend={"font": {"size": 22}, "orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "center", "x": 0.5}, |
92 | | - template="plotly_white", |
93 | | - plot_bgcolor="white", |
94 | | - paper_bgcolor="white", |
| 124 | + paper_bgcolor=PAGE_BG, |
| 125 | + plot_bgcolor=PAGE_BG, |
95 | 126 | hovermode="x unified", |
96 | | - margin={"l": 100, "r": 50, "t": 120, "b": 80}, |
| 127 | + margin={"l": 60, "r": 50, "t": 140, "b": 120}, |
97 | 128 | ) |
98 | 129 |
|
99 | | -# Save as PNG (4800x2700 px) |
100 | | -fig.write_image("plot.png", width=1600, height=900, scale=3) |
101 | | - |
102 | | -# Save interactive HTML |
103 | | -fig.write_html("plot.html", include_plotlyjs="cdn") |
| 130 | +# Save |
| 131 | +fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3) |
| 132 | +fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") |
0 commit comments