Skip to content

Commit 289b7ae

Browse files
feat(bokeh): implement sankey-basic (#5602)
## Implementation: `sankey-basic` - python/bokeh Implements the **python/bokeh** version of `sankey-basic`. **File:** `plots/sankey-basic/implementations/python/bokeh.py` **Parent Issue:** #810 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25156351219)* --------- 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 7062d53 commit 289b7ae

2 files changed

Lines changed: 228 additions & 188 deletions

File tree

Lines changed: 59 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

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+
714
import numpy as np
8-
from bokeh.io import export_png, save
15+
from bokeh.io import export_png, output_file, save
916
from bokeh.models import Label
1017
from bokeh.plotting import figure
1118

1219

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+
1330
# Data - Energy flow from sources to sectors (TWh)
1431
flows = [
1532
{"source": "Coal", "target": "Industrial", "value": 25},
@@ -34,21 +51,11 @@
3451
if f["target"] not in targets:
3552
targets.append(f["target"])
3653

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"}
5259

5360
# Calculate totals for node sizing
5461
source_totals = {s: sum(f["value"] for f in flows if f["source"] == s) for s in sources}
@@ -64,37 +71,37 @@
6471

6572
# Calculate node positions for sources (left side)
6673
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
6875

6976
source_nodes = {}
7077
current_y = padding_y
7178
for s in sources:
72-
height = source_totals[s] * scale_factor
79+
height = source_totals[s] * scale_src
7380
source_nodes[s] = {"x": left_x, "y": current_y, "height": height, "value": source_totals[s]}
7481
current_y += height + node_gap
7582

7683
# Calculate node positions for targets (right side)
7784
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
7986

8087
target_nodes = {}
8188
current_y = padding_y
8289
for t in targets:
83-
height = target_totals[t] * scale_factor_t
90+
height = target_totals[t] * scale_tgt
8491
target_nodes[t] = {"x": right_x - node_width, "y": current_y, "height": height, "value": target_totals[t]}
8592
current_y += height + node_gap
8693

8794
# 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)
9097

91-
# Create figure (4800 × 2700 px)
98+
# Plot
9299
p = figure(
93100
width=4800,
94101
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),
98105
tools="",
99106
toolbar_location=None,
100107
)
@@ -105,52 +112,41 @@
105112
tgt = f["target"]
106113
value = f["value"]
107114

108-
# Get node info
109115
src_node = source_nodes[src]
110116
tgt_node = target_nodes[tgt]
111117

112-
# Flow height proportional to value
113118
src_flow_height = (value / source_totals[src]) * src_node["height"]
114119
tgt_flow_height = (value / target_totals[tgt]) * tgt_node["height"]
115120

116-
# Source connection points
117121
x0 = src_node["x"] + node_width
118122
y0_bottom = src_node["y"] + source_offsets[src]
119123
y0_top = y0_bottom + src_flow_height
120124

121-
# Target connection points
122125
x1 = tgt_node["x"]
123126
y1_bottom = tgt_node["y"] + target_offsets[tgt]
124127
y1_top = y1_bottom + tgt_flow_height
125128

126-
# Update offsets for stacking
127129
source_offsets[src] += src_flow_height
128130
target_offsets[tgt] += tgt_flow_height
129131

130-
# Create smooth bezier flow path
131-
t = np.linspace(0, 1, 50)
132+
t = np.linspace(0, 1, 60)
132133
cx0 = x0 + (x1 - x0) * 0.4
133134
cx1 = x0 + (x1 - x0) * 0.6
134135

135-
# Cubic bezier for x positions
136136
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
139137
y_bottom = (1 - t) * y0_bottom + t * y1_bottom
140138
y_top = (1 - t) * y0_top + t * y1_top
141139

142-
# Create closed polygon
143140
xs = list(x_path) + list(x_path[::-1])
144141
ys = list(y_top) + list(y_bottom[::-1])
145142

146-
# Draw flow with source color and transparency
147143
p.patch(
148144
xs,
149145
ys,
150146
fill_color=source_colors[src],
151-
fill_alpha=0.5,
147+
fill_alpha=0.45,
152148
line_color=source_colors[src],
153-
line_alpha=0.7,
149+
line_alpha=0.6,
154150
line_width=1,
155151
)
156152

@@ -163,19 +159,19 @@
163159
bottom=node["y"],
164160
top=node["y"] + node["height"],
165161
fill_color=source_colors[s],
166-
fill_alpha=0.9,
167-
line_color="white",
162+
fill_alpha=0.92,
163+
line_color=PAGE_BG,
168164
line_width=2,
169165
)
170-
# Add label to the left of node
171166
label = Label(
172-
x=node["x"] - 1,
167+
x=node["x"] - 1.5,
173168
y=node["y"] + node["height"] / 2,
174169
text=f"{s} ({node['value']} TWh)",
175170
text_font_size="22pt",
176171
text_align="right",
177172
text_baseline="middle",
178-
text_color="#333333",
173+
text_color=INK,
174+
text_font="helvetica",
179175
)
180176
p.add_layout(label)
181177

@@ -187,38 +183,39 @@
187183
right=node["x"] + node_width,
188184
bottom=node["y"],
189185
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,
193189
line_width=2,
194190
)
195-
# Add label to the right of node
196191
label = Label(
197-
x=node["x"] + node_width + 1,
192+
x=node["x"] + node_width + 1.5,
198193
y=node["y"] + node["height"] / 2,
199194
text=f"{t} ({node['value']} TWh)",
200195
text_font_size="22pt",
201196
text_align="left",
202197
text_baseline="middle",
203-
text_color="#333333",
198+
text_color=INK,
199+
text_font="helvetica",
204200
)
205201
p.add_layout(label)
206202

207-
# Styling
203+
# Style — theme-adaptive chrome
208204
p.title.text_font_size = "32pt"
205+
p.title.text_color = INK
209206
p.title.align = "center"
207+
p.title.text_font = "helvetica"
210208

211-
# Hide axes for cleaner Sankey look
212209
p.xaxis.visible = False
213210
p.yaxis.visible = False
214211
p.xgrid.visible = False
215212
p.ygrid.visible = False
216213
p.outline_line_color = None
217214

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
221217

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

Comments
 (0)