|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import pandas as pd |
8 | 10 | from lets_plot import ( |
9 | 11 | LetsPlot, |
10 | 12 | aes, |
11 | 13 | element_blank, |
| 14 | + element_rect, |
12 | 15 | element_text, |
13 | 16 | geom_polygon, |
14 | 17 | geom_rect, |
|
27 | 30 |
|
28 | 31 | LetsPlot.setup_html() |
29 | 32 |
|
| 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 | + |
30 | 43 | # Energy flow data: sources -> sectors (realistic energy distribution) |
31 | 44 | flows = [ |
32 | 45 | ("Coal", "Industrial", 28), |
|
41 | 54 | ("Renewable", "Industrial", 6), |
42 | 55 | ] |
43 | 56 |
|
44 | | -# Define node ordering |
45 | 57 | sources = ["Coal", "Natural Gas", "Nuclear", "Renewable"] |
46 | 58 | targets = ["Industrial", "Residential", "Commercial"] |
| 59 | +source_color_map = dict(zip(sources, OKABE_ITO, strict=True)) |
47 | 60 |
|
48 | 61 | # Calculate totals for each node |
49 | 62 | source_totals = {} |
|
54 | 67 | for _, tgt, val in flows: |
55 | 68 | target_totals[tgt] = target_totals.get(tgt, 0) + val |
56 | 69 |
|
57 | | -# Normalize positions |
| 70 | +# Layout parameters |
58 | 71 | total_flow = sum(v for _, _, v in flows) |
59 | 72 | node_gap = 0.04 |
60 | 73 | x_left = 0.18 |
|
80 | 93 | source_offsets = dict.fromkeys(sources, 0) |
81 | 94 | target_offsets = dict.fromkeys(targets, 0) |
82 | 95 |
|
83 | | -# Build flow polygons with smooth bezier curves |
| 96 | +# Build flow polygons with smooth cubic bezier curves |
84 | 97 | flow_data = [] |
85 | 98 |
|
86 | 99 | for src, tgt, val in flows: |
87 | 100 | flow_height = val / total_flow * 0.85 |
88 | 101 |
|
89 | | - # Source connection points |
90 | 102 | src_y0 = source_positions[src]["y0"] + source_offsets[src] |
91 | 103 | src_y1 = src_y0 + flow_height |
92 | 104 | source_offsets[src] += flow_height |
93 | 105 |
|
94 | | - # Target connection points |
95 | 106 | tgt_y0 = target_positions[tgt]["y0"] + target_offsets[tgt] |
96 | 107 | tgt_y1 = tgt_y0 + flow_height |
97 | 108 | target_offsets[tgt] += flow_height |
98 | 109 |
|
99 | | - # Create smooth bezier polygon for flow |
100 | 110 | 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 = [], [] |
105 | 113 |
|
106 | 114 | for i in range(n_points + 1): |
107 | 115 | t = i / n_points |
108 | 116 | x = x_left + t * (x_right - x_left) |
109 | | - # Smooth cubic bezier easing |
110 | 117 | 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 | | - |
114 | 118 | x_vals_top.append(x) |
115 | | - y_vals_top.append(y_top) |
| 119 | + y_vals_top.append(src_y1 + ease * (tgt_y1 - src_y1)) |
116 | 120 | x_vals_bottom.append(x) |
117 | | - y_vals_bottom.append(y_bottom) |
| 121 | + y_vals_bottom.append(src_y0 + ease * (tgt_y0 - src_y0)) |
118 | 122 |
|
119 | | - # Combine into closed polygon |
120 | 123 | x_polygon = x_vals_top + x_vals_bottom[::-1] |
121 | 124 | y_polygon = y_vals_top + y_vals_bottom[::-1] |
122 | 125 |
|
|
132 | 135 | for src in sources: |
133 | 136 | pos = source_positions[src] |
134 | 137 | 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"]} |
143 | 139 | ) |
144 | 140 |
|
145 | 141 | for tgt in targets: |
146 | 142 | pos = target_positions[tgt] |
147 | 143 | 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"]} |
156 | 145 | ) |
157 | 146 |
|
158 | 147 | df_nodes = pd.DataFrame(node_rects) |
159 | 148 |
|
160 | | -# Build labels with flow values |
| 149 | +# Build labels with flow totals |
161 | 150 | labels = [] |
162 | 151 | for src in sources: |
163 | 152 | pos = source_positions[src] |
|
183 | 172 |
|
184 | 173 | df_labels = pd.DataFrame(labels) |
185 | 174 |
|
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 |
190 | 176 | plot = ( |
191 | 177 | ggplot() |
192 | 178 | + 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 |
201 | 180 | ) |
| 181 | + + geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), data=df_nodes, fill=INK, color=INK, size=1.5) |
202 | 182 | + geom_text( |
203 | 183 | aes(x="x", y="y", label="label"), |
204 | 184 | data=df_labels[df_labels["side"] == "left"], |
205 | 185 | size=14, |
206 | 186 | hjust=1, |
| 187 | + color=INK_SOFT, |
207 | 188 | family="sans-serif", |
208 | 189 | ) |
209 | 190 | + geom_text( |
210 | 191 | aes(x="x", y="y", label="label"), |
211 | 192 | data=df_labels[df_labels["side"] == "right"], |
212 | 193 | size=14, |
213 | 194 | hjust=0, |
| 195 | + color=INK_SOFT, |
214 | 196 | family="sans-serif", |
215 | 197 | ) |
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") |
218 | 200 | + theme_minimal() |
219 | 201 | + 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), |
221 | 205 | axis_title=element_blank(), |
222 | 206 | axis_text=element_blank(), |
223 | 207 | axis_ticks=element_blank(), |
224 | 208 | 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), |
227 | 211 | legend_position="bottom", |
| 212 | + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), |
228 | 213 | ) |
229 | 214 | + scale_x_continuous(limits=[-0.02, 1.02]) |
230 | 215 | + scale_y_continuous(limits=[-0.02, 1.02]) |
231 | 216 | + ggsize(1600, 900) |
232 | 217 | ) |
233 | 218 |
|
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