|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | sankey-basic: Basic Sankey Diagram |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.9.0 | Python 3.13.13 |
| 4 | +Quality: 86/100 | Updated: 2026-04-30 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import sys |
| 9 | + |
| 10 | + |
| 11 | +_script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 12 | +sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _script_dir] |
| 13 | + |
7 | 14 | import numpy as np |
8 | | -from bokeh.io import export_png, save |
| 15 | +from bokeh.io import export_png, output_file, save |
9 | 16 | from bokeh.models import Label |
10 | 17 | from bokeh.plotting import figure |
11 | 18 |
|
12 | 19 |
|
| 20 | +# Theme tokens |
| 21 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 22 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 23 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 24 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 25 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 26 | + |
| 27 | +# Okabe-Ito palette — first source always #009E73 |
| 28 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"] |
| 29 | + |
13 | 30 | # Data - Energy flow from sources to sectors (TWh) |
14 | 31 | flows = [ |
15 | 32 | {"source": "Coal", "target": "Industrial", "value": 25}, |
|
34 | 51 | if f["target"] not in targets: |
35 | 52 | targets.append(f["target"]) |
36 | 53 |
|
37 | | -# Color palette for sources (Python Blue first, then colorblind-safe) |
38 | | -source_colors = { |
39 | | - "Coal": "#306998", # Python Blue |
40 | | - "Gas": "#FFD43B", # Python Yellow |
41 | | - "Nuclear": "#9B59B6", # Purple |
42 | | - "Hydro": "#3498DB", # Light blue |
43 | | - "Solar": "#E67E22", # Orange |
44 | | -} |
45 | | - |
46 | | -# Target colors (darker shades) |
47 | | -target_colors = { |
48 | | - "Industrial": "#2C3E50", # Dark blue-grey |
49 | | - "Commercial": "#1ABC9C", # Teal |
50 | | - "Residential": "#E74C3C", # Red |
51 | | -} |
| 54 | +# Source colors: Okabe-Ito in canonical order |
| 55 | +source_colors = {s: OKABE_ITO[i] for i, s in enumerate(sources)} |
| 56 | + |
| 57 | +# Target node colors: slightly muted variants of INK_SOFT family |
| 58 | +target_node_colors = {"Industrial": "#5A6A7A", "Commercial": "#7A6A8A", "Residential": "#6A7A5A"} |
52 | 59 |
|
53 | 60 | # Calculate totals for node sizing |
54 | 61 | source_totals = {s: sum(f["value"] for f in flows if f["source"] == s) for s in sources} |
|
64 | 71 |
|
65 | 72 | # Calculate node positions for sources (left side) |
66 | 73 | source_height_total = sum(source_totals.values()) |
67 | | -scale_factor = (total_height - 2 * padding_y - (len(sources) - 1) * node_gap) / source_height_total |
| 74 | +scale_src = (total_height - 2 * padding_y - (len(sources) - 1) * node_gap) / source_height_total |
68 | 75 |
|
69 | 76 | source_nodes = {} |
70 | 77 | current_y = padding_y |
71 | 78 | for s in sources: |
72 | | - height = source_totals[s] * scale_factor |
| 79 | + height = source_totals[s] * scale_src |
73 | 80 | source_nodes[s] = {"x": left_x, "y": current_y, "height": height, "value": source_totals[s]} |
74 | 81 | current_y += height + node_gap |
75 | 82 |
|
76 | 83 | # Calculate node positions for targets (right side) |
77 | 84 | target_height_total = sum(target_totals.values()) |
78 | | -scale_factor_t = (total_height - 2 * padding_y - (len(targets) - 1) * node_gap) / target_height_total |
| 85 | +scale_tgt = (total_height - 2 * padding_y - (len(targets) - 1) * node_gap) / target_height_total |
79 | 86 |
|
80 | 87 | target_nodes = {} |
81 | 88 | current_y = padding_y |
82 | 89 | for t in targets: |
83 | | - height = target_totals[t] * scale_factor_t |
| 90 | + height = target_totals[t] * scale_tgt |
84 | 91 | target_nodes[t] = {"x": right_x - node_width, "y": current_y, "height": height, "value": target_totals[t]} |
85 | 92 | current_y += height + node_gap |
86 | 93 |
|
87 | 94 | # Track flow offsets for stacking flows at each node |
88 | | -source_offsets = dict.fromkeys(sources, 0) |
89 | | -target_offsets = dict.fromkeys(targets, 0) |
| 95 | +source_offsets = dict.fromkeys(sources, 0.0) |
| 96 | +target_offsets = dict.fromkeys(targets, 0.0) |
90 | 97 |
|
91 | | -# Create figure (4800 × 2700 px) |
| 98 | +# Plot |
92 | 99 | p = figure( |
93 | 100 | width=4800, |
94 | 101 | height=2700, |
95 | | - title="Energy Flow · sankey-basic · bokeh · pyplots.ai", |
96 | | - x_range=(-15, 115), |
97 | | - y_range=(-5, 105), |
| 102 | + title="Energy Flow · sankey-basic · bokeh · anyplot.ai", |
| 103 | + x_range=(-18, 118), |
| 104 | + y_range=(-5, 108), |
98 | 105 | tools="", |
99 | 106 | toolbar_location=None, |
100 | 107 | ) |
|
105 | 112 | tgt = f["target"] |
106 | 113 | value = f["value"] |
107 | 114 |
|
108 | | - # Get node info |
109 | 115 | src_node = source_nodes[src] |
110 | 116 | tgt_node = target_nodes[tgt] |
111 | 117 |
|
112 | | - # Flow height proportional to value |
113 | 118 | src_flow_height = (value / source_totals[src]) * src_node["height"] |
114 | 119 | tgt_flow_height = (value / target_totals[tgt]) * tgt_node["height"] |
115 | 120 |
|
116 | | - # Source connection points |
117 | 121 | x0 = src_node["x"] + node_width |
118 | 122 | y0_bottom = src_node["y"] + source_offsets[src] |
119 | 123 | y0_top = y0_bottom + src_flow_height |
120 | 124 |
|
121 | | - # Target connection points |
122 | 125 | x1 = tgt_node["x"] |
123 | 126 | y1_bottom = tgt_node["y"] + target_offsets[tgt] |
124 | 127 | y1_top = y1_bottom + tgt_flow_height |
125 | 128 |
|
126 | | - # Update offsets for stacking |
127 | 129 | source_offsets[src] += src_flow_height |
128 | 130 | target_offsets[tgt] += tgt_flow_height |
129 | 131 |
|
130 | | - # Create smooth bezier flow path |
131 | | - t = np.linspace(0, 1, 50) |
| 132 | + t = np.linspace(0, 1, 60) |
132 | 133 | cx0 = x0 + (x1 - x0) * 0.4 |
133 | 134 | cx1 = x0 + (x1 - x0) * 0.6 |
134 | 135 |
|
135 | | - # Cubic bezier for x positions |
136 | 136 | x_path = (1 - t) ** 3 * x0 + 3 * (1 - t) ** 2 * t * cx0 + 3 * (1 - t) * t**2 * cx1 + t**3 * x1 |
137 | | - |
138 | | - # Linear interpolation for y positions |
139 | 137 | y_bottom = (1 - t) * y0_bottom + t * y1_bottom |
140 | 138 | y_top = (1 - t) * y0_top + t * y1_top |
141 | 139 |
|
142 | | - # Create closed polygon |
143 | 140 | xs = list(x_path) + list(x_path[::-1]) |
144 | 141 | ys = list(y_top) + list(y_bottom[::-1]) |
145 | 142 |
|
146 | | - # Draw flow with source color and transparency |
147 | 143 | p.patch( |
148 | 144 | xs, |
149 | 145 | ys, |
150 | 146 | fill_color=source_colors[src], |
151 | | - fill_alpha=0.5, |
| 147 | + fill_alpha=0.45, |
152 | 148 | line_color=source_colors[src], |
153 | | - line_alpha=0.7, |
| 149 | + line_alpha=0.6, |
154 | 150 | line_width=1, |
155 | 151 | ) |
156 | 152 |
|
|
163 | 159 | bottom=node["y"], |
164 | 160 | top=node["y"] + node["height"], |
165 | 161 | fill_color=source_colors[s], |
166 | | - fill_alpha=0.9, |
167 | | - line_color="white", |
| 162 | + fill_alpha=0.92, |
| 163 | + line_color=PAGE_BG, |
168 | 164 | line_width=2, |
169 | 165 | ) |
170 | | - # Add label to the left of node |
171 | 166 | label = Label( |
172 | | - x=node["x"] - 1, |
| 167 | + x=node["x"] - 1.5, |
173 | 168 | y=node["y"] + node["height"] / 2, |
174 | 169 | text=f"{s} ({node['value']} TWh)", |
175 | 170 | text_font_size="22pt", |
176 | 171 | text_align="right", |
177 | 172 | text_baseline="middle", |
178 | | - text_color="#333333", |
| 173 | + text_color=INK, |
| 174 | + text_font="helvetica", |
179 | 175 | ) |
180 | 176 | p.add_layout(label) |
181 | 177 |
|
|
187 | 183 | right=node["x"] + node_width, |
188 | 184 | bottom=node["y"], |
189 | 185 | top=node["y"] + node["height"], |
190 | | - fill_color=target_colors[t], |
191 | | - fill_alpha=0.9, |
192 | | - line_color="white", |
| 186 | + fill_color=target_node_colors[t], |
| 187 | + fill_alpha=0.92, |
| 188 | + line_color=PAGE_BG, |
193 | 189 | line_width=2, |
194 | 190 | ) |
195 | | - # Add label to the right of node |
196 | 191 | label = Label( |
197 | | - x=node["x"] + node_width + 1, |
| 192 | + x=node["x"] + node_width + 1.5, |
198 | 193 | y=node["y"] + node["height"] / 2, |
199 | 194 | text=f"{t} ({node['value']} TWh)", |
200 | 195 | text_font_size="22pt", |
201 | 196 | text_align="left", |
202 | 197 | text_baseline="middle", |
203 | | - text_color="#333333", |
| 198 | + text_color=INK, |
| 199 | + text_font="helvetica", |
204 | 200 | ) |
205 | 201 | p.add_layout(label) |
206 | 202 |
|
207 | | -# Styling |
| 203 | +# Style — theme-adaptive chrome |
208 | 204 | p.title.text_font_size = "32pt" |
| 205 | +p.title.text_color = INK |
209 | 206 | p.title.align = "center" |
| 207 | +p.title.text_font = "helvetica" |
210 | 208 |
|
211 | | -# Hide axes for cleaner Sankey look |
212 | 209 | p.xaxis.visible = False |
213 | 210 | p.yaxis.visible = False |
214 | 211 | p.xgrid.visible = False |
215 | 212 | p.ygrid.visible = False |
216 | 213 | p.outline_line_color = None |
217 | 214 |
|
218 | | -# Background |
219 | | -p.background_fill_color = "#FAFAFA" |
220 | | -p.border_fill_color = "#FFFFFF" |
| 215 | +p.background_fill_color = PAGE_BG |
| 216 | +p.border_fill_color = PAGE_BG |
221 | 217 |
|
222 | | -# Save outputs |
223 | | -export_png(p, filename="plot.png") |
224 | | -save(p, filename="plot.html") |
| 218 | +# Save |
| 219 | +export_png(p, filename=f"plot-{THEME}.png") |
| 220 | +output_file(f"plot-{THEME}.html") |
| 221 | +save(p) |
0 commit comments