Skip to content

Commit 92946f4

Browse files
feat(letsplot): implement sankey-basic (#5608)
## Implementation: `sankey-basic` - python/letsplot Implements the **python/letsplot** version of `sankey-basic`. **File:** `plots/sankey-basic/implementations/python/letsplot.py` **Parent Issue:** #810 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25156796686)* --------- 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 b64626f commit 92946f4

2 files changed

Lines changed: 227 additions & 191 deletions

File tree

plots/sankey-basic/implementations/python/letsplot.py

Lines changed: 42 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
sankey-basic: Basic Sankey Diagram
3-
Library: letsplot 4.8.2 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: letsplot 4.9.0 | Python 3.13.13
4+
Quality: 85/100 | Updated: 2026-04-30
55
"""
66

7+
import os
8+
79
import pandas as pd
810
from lets_plot import (
911
LetsPlot,
1012
aes,
1113
element_blank,
14+
element_rect,
1215
element_text,
1316
geom_polygon,
1417
geom_rect,
@@ -27,6 +30,16 @@
2730

2831
LetsPlot.setup_html()
2932

33+
# Theme tokens
34+
THEME = os.getenv("ANYPLOT_THEME", "light")
35+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
36+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
37+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
38+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
39+
40+
# Okabe-Ito palette for source categories (canonical order, first = #009E73)
41+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"]
42+
3043
# Energy flow data: sources -> sectors (realistic energy distribution)
3144
flows = [
3245
("Coal", "Industrial", 28),
@@ -41,9 +54,9 @@
4154
("Renewable", "Industrial", 6),
4255
]
4356

44-
# Define node ordering
4557
sources = ["Coal", "Natural Gas", "Nuclear", "Renewable"]
4658
targets = ["Industrial", "Residential", "Commercial"]
59+
source_color_map = dict(zip(sources, OKABE_ITO, strict=True))
4760

4861
# Calculate totals for each node
4962
source_totals = {}
@@ -54,7 +67,7 @@
5467
for _, tgt, val in flows:
5568
target_totals[tgt] = target_totals.get(tgt, 0) + val
5669

57-
# Normalize positions
70+
# Layout parameters
5871
total_flow = sum(v for _, _, v in flows)
5972
node_gap = 0.04
6073
x_left = 0.18
@@ -80,43 +93,33 @@
8093
source_offsets = dict.fromkeys(sources, 0)
8194
target_offsets = dict.fromkeys(targets, 0)
8295

83-
# Build flow polygons with smooth bezier curves
96+
# Build flow polygons with smooth cubic bezier curves
8497
flow_data = []
8598

8699
for src, tgt, val in flows:
87100
flow_height = val / total_flow * 0.85
88101

89-
# Source connection points
90102
src_y0 = source_positions[src]["y0"] + source_offsets[src]
91103
src_y1 = src_y0 + flow_height
92104
source_offsets[src] += flow_height
93105

94-
# Target connection points
95106
tgt_y0 = target_positions[tgt]["y0"] + target_offsets[tgt]
96107
tgt_y1 = tgt_y0 + flow_height
97108
target_offsets[tgt] += flow_height
98109

99-
# Create smooth bezier polygon for flow
100110
n_points = 40
101-
x_vals_top = []
102-
y_vals_top = []
103-
x_vals_bottom = []
104-
y_vals_bottom = []
111+
x_vals_top, y_vals_top = [], []
112+
x_vals_bottom, y_vals_bottom = [], []
105113

106114
for i in range(n_points + 1):
107115
t = i / n_points
108116
x = x_left + t * (x_right - x_left)
109-
# Smooth cubic bezier easing
110117
ease = t * t * (3 - 2 * t)
111-
y_top = src_y1 + ease * (tgt_y1 - src_y1)
112-
y_bottom = src_y0 + ease * (tgt_y0 - src_y0)
113-
114118
x_vals_top.append(x)
115-
y_vals_top.append(y_top)
119+
y_vals_top.append(src_y1 + ease * (tgt_y1 - src_y1))
116120
x_vals_bottom.append(x)
117-
y_vals_bottom.append(y_bottom)
121+
y_vals_bottom.append(src_y0 + ease * (tgt_y0 - src_y0))
118122

119-
# Combine into closed polygon
120123
x_polygon = x_vals_top + x_vals_bottom[::-1]
121124
y_polygon = y_vals_top + y_vals_bottom[::-1]
122125

@@ -132,32 +135,18 @@
132135
for src in sources:
133136
pos = source_positions[src]
134137
node_rects.append(
135-
{
136-
"xmin": pos["x"] - node_width / 2,
137-
"xmax": pos["x"] + node_width / 2,
138-
"ymin": pos["y0"],
139-
"ymax": pos["y1"],
140-
"label": src,
141-
"side": "source",
142-
}
138+
{"xmin": pos["x"] - node_width / 2, "xmax": pos["x"] + node_width / 2, "ymin": pos["y0"], "ymax": pos["y1"]}
143139
)
144140

145141
for tgt in targets:
146142
pos = target_positions[tgt]
147143
node_rects.append(
148-
{
149-
"xmin": pos["x"] - node_width / 2,
150-
"xmax": pos["x"] + node_width / 2,
151-
"ymin": pos["y0"],
152-
"ymax": pos["y1"],
153-
"label": tgt,
154-
"side": "target",
155-
}
144+
{"xmin": pos["x"] - node_width / 2, "xmax": pos["x"] + node_width / 2, "ymin": pos["y0"], "ymax": pos["y1"]}
156145
)
157146

158147
df_nodes = pd.DataFrame(node_rects)
159148

160-
# Build labels with flow values
149+
# Build labels with flow totals
161150
labels = []
162151
for src in sources:
163152
pos = source_positions[src]
@@ -183,56 +172,50 @@
183172

184173
df_labels = pd.DataFrame(labels)
185174

186-
# Colors for each energy source
187-
source_colors = {"Coal": "#4A4A4A", "Natural Gas": "#306998", "Nuclear": "#9B59B6", "Renewable": "#27AE60"}
188-
189-
# Create the plot
175+
# Plot
190176
plot = (
191177
ggplot()
192178
+ geom_polygon(
193-
aes(x="x", y="y", group="flow_id", fill="source"), data=df_flows, alpha=0.65, color="white", size=0.2
194-
)
195-
+ geom_rect(
196-
aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"),
197-
data=df_nodes,
198-
fill="#2C3E50",
199-
color="#1A252F",
200-
size=1.5,
179+
aes(x="x", y="y", group="flow_id", fill="source"), data=df_flows, alpha=0.65, color=PAGE_BG, size=0.2
201180
)
181+
+ geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), data=df_nodes, fill=INK, color=INK, size=1.5)
202182
+ geom_text(
203183
aes(x="x", y="y", label="label"),
204184
data=df_labels[df_labels["side"] == "left"],
205185
size=14,
206186
hjust=1,
187+
color=INK_SOFT,
207188
family="sans-serif",
208189
)
209190
+ geom_text(
210191
aes(x="x", y="y", label="label"),
211192
data=df_labels[df_labels["side"] == "right"],
212193
size=14,
213194
hjust=0,
195+
color=INK_SOFT,
214196
family="sans-serif",
215197
)
216-
+ scale_fill_manual(values=[source_colors[s] for s in sources], name="Energy Source")
217-
+ labs(title="Energy Flow · sankey-basic · letsplot · pyplots.ai")
198+
+ scale_fill_manual(values=[source_color_map[s] for s in sources], name="Energy Source")
199+
+ labs(title="Energy Flow · sankey-basic · letsplot · anyplot.ai")
218200
+ theme_minimal()
219201
+ theme(
220-
plot_title=element_text(size=30, face="bold"),
202+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
203+
panel_background=element_rect(fill=PAGE_BG),
204+
plot_title=element_text(size=30, face="bold", color=INK),
221205
axis_title=element_blank(),
222206
axis_text=element_blank(),
223207
axis_ticks=element_blank(),
224208
panel_grid=element_blank(),
225-
legend_text=element_text(size=18),
226-
legend_title=element_text(size=20, face="bold"),
209+
legend_text=element_text(size=18, color=INK_SOFT),
210+
legend_title=element_text(size=20, face="bold", color=INK),
227211
legend_position="bottom",
212+
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
228213
)
229214
+ scale_x_continuous(limits=[-0.02, 1.02])
230215
+ scale_y_continuous(limits=[-0.02, 1.02])
231216
+ ggsize(1600, 900)
232217
)
233218

234-
# Save as PNG (scale 3x for 4800 × 2700 px)
235-
ggsave(plot, "plot.png", path=".", scale=3)
236-
237-
# Save as HTML for interactivity
238-
ggsave(plot, "plot.html", path=".")
219+
# Save PNG (scale 3x for 4800 × 2700 px) and HTML
220+
ggsave(plot, f"plot-{THEME}.png", path=".", scale=3)
221+
ggsave(plot, f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)