Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 59 additions & 62 deletions plots/sankey-basic/implementations/python/bokeh.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
""" pyplots.ai
""" anyplot.ai
sankey-basic: Basic Sankey Diagram
Library: bokeh 3.8.1 | Python 3.13.11
Quality: 91/100 | Created: 2025-12-23
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 86/100 | Updated: 2026-04-30
"""

import os
import sys


_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _script_dir]

import numpy as np
from bokeh.io import export_png, save
from bokeh.io import export_png, output_file, save
from bokeh.models import Label
from bokeh.plotting import figure


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Okabe-Ito palette — first source always #009E73
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]

# Data - Energy flow from sources to sectors (TWh)
flows = [
{"source": "Coal", "target": "Industrial", "value": 25},
Expand All @@ -34,21 +51,11 @@
if f["target"] not in targets:
targets.append(f["target"])

# Color palette for sources (Python Blue first, then colorblind-safe)
source_colors = {
"Coal": "#306998", # Python Blue
"Gas": "#FFD43B", # Python Yellow
"Nuclear": "#9B59B6", # Purple
"Hydro": "#3498DB", # Light blue
"Solar": "#E67E22", # Orange
}

# Target colors (darker shades)
target_colors = {
"Industrial": "#2C3E50", # Dark blue-grey
"Commercial": "#1ABC9C", # Teal
"Residential": "#E74C3C", # Red
}
# Source colors: Okabe-Ito in canonical order
source_colors = {s: OKABE_ITO[i] for i, s in enumerate(sources)}

# Target node colors: slightly muted variants of INK_SOFT family
target_node_colors = {"Industrial": "#5A6A7A", "Commercial": "#7A6A8A", "Residential": "#6A7A5A"}

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

# Calculate node positions for sources (left side)
source_height_total = sum(source_totals.values())
scale_factor = (total_height - 2 * padding_y - (len(sources) - 1) * node_gap) / source_height_total
scale_src = (total_height - 2 * padding_y - (len(sources) - 1) * node_gap) / source_height_total

source_nodes = {}
current_y = padding_y
for s in sources:
height = source_totals[s] * scale_factor
height = source_totals[s] * scale_src
source_nodes[s] = {"x": left_x, "y": current_y, "height": height, "value": source_totals[s]}
current_y += height + node_gap

# Calculate node positions for targets (right side)
target_height_total = sum(target_totals.values())
scale_factor_t = (total_height - 2 * padding_y - (len(targets) - 1) * node_gap) / target_height_total
scale_tgt = (total_height - 2 * padding_y - (len(targets) - 1) * node_gap) / target_height_total

target_nodes = {}
current_y = padding_y
for t in targets:
height = target_totals[t] * scale_factor_t
height = target_totals[t] * scale_tgt
target_nodes[t] = {"x": right_x - node_width, "y": current_y, "height": height, "value": target_totals[t]}
current_y += height + node_gap

# Track flow offsets for stacking flows at each node
source_offsets = dict.fromkeys(sources, 0)
target_offsets = dict.fromkeys(targets, 0)
source_offsets = dict.fromkeys(sources, 0.0)
target_offsets = dict.fromkeys(targets, 0.0)

# Create figure (4800 × 2700 px)
# Plot
p = figure(
width=4800,
height=2700,
title="Energy Flow · sankey-basic · bokeh · pyplots.ai",
x_range=(-15, 115),
y_range=(-5, 105),
title="Energy Flow · sankey-basic · bokeh · anyplot.ai",
x_range=(-18, 118),
y_range=(-5, 108),
tools="",
toolbar_location=None,
)
Expand All @@ -105,52 +112,41 @@
tgt = f["target"]
value = f["value"]

# Get node info
src_node = source_nodes[src]
tgt_node = target_nodes[tgt]

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

# Source connection points
x0 = src_node["x"] + node_width
y0_bottom = src_node["y"] + source_offsets[src]
y0_top = y0_bottom + src_flow_height

# Target connection points
x1 = tgt_node["x"]
y1_bottom = tgt_node["y"] + target_offsets[tgt]
y1_top = y1_bottom + tgt_flow_height

# Update offsets for stacking
source_offsets[src] += src_flow_height
target_offsets[tgt] += tgt_flow_height

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

# Cubic bezier for x positions
x_path = (1 - t) ** 3 * x0 + 3 * (1 - t) ** 2 * t * cx0 + 3 * (1 - t) * t**2 * cx1 + t**3 * x1

# Linear interpolation for y positions
y_bottom = (1 - t) * y0_bottom + t * y1_bottom
y_top = (1 - t) * y0_top + t * y1_top

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

# Draw flow with source color and transparency
p.patch(
xs,
ys,
fill_color=source_colors[src],
fill_alpha=0.5,
fill_alpha=0.45,
line_color=source_colors[src],
line_alpha=0.7,
line_alpha=0.6,
line_width=1,
)

Expand All @@ -163,19 +159,19 @@
bottom=node["y"],
top=node["y"] + node["height"],
fill_color=source_colors[s],
fill_alpha=0.9,
line_color="white",
fill_alpha=0.92,
line_color=PAGE_BG,
line_width=2,
)
# Add label to the left of node
label = Label(
x=node["x"] - 1,
x=node["x"] - 1.5,
y=node["y"] + node["height"] / 2,
text=f"{s} ({node['value']} TWh)",
text_font_size="22pt",
text_align="right",
text_baseline="middle",
text_color="#333333",
text_color=INK,
text_font="helvetica",
)
p.add_layout(label)

Expand All @@ -187,38 +183,39 @@
right=node["x"] + node_width,
bottom=node["y"],
top=node["y"] + node["height"],
fill_color=target_colors[t],
fill_alpha=0.9,
line_color="white",
fill_color=target_node_colors[t],
fill_alpha=0.92,
line_color=PAGE_BG,
line_width=2,
)
# Add label to the right of node
label = Label(
x=node["x"] + node_width + 1,
x=node["x"] + node_width + 1.5,
y=node["y"] + node["height"] / 2,
text=f"{t} ({node['value']} TWh)",
text_font_size="22pt",
text_align="left",
text_baseline="middle",
text_color="#333333",
text_color=INK,
text_font="helvetica",
)
p.add_layout(label)

# Styling
# Style — theme-adaptive chrome
p.title.text_font_size = "32pt"
p.title.text_color = INK
p.title.align = "center"
p.title.text_font = "helvetica"

# Hide axes for cleaner Sankey look
p.xaxis.visible = False
p.yaxis.visible = False
p.xgrid.visible = False
p.ygrid.visible = False
p.outline_line_color = None

# Background
p.background_fill_color = "#FAFAFA"
p.border_fill_color = "#FFFFFF"
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

# Save outputs
export_png(p, filename="plot.png")
save(p, filename="plot.html")
# Save
export_png(p, filename=f"plot-{THEME}.png")
output_file(f"plot-{THEME}.html")
save(p)
Loading
Loading