Skip to content

Commit 781d3fe

Browse files
feat(bokeh): implement waterfall-basic (#5742)
## Implementation: `waterfall-basic` - python/bokeh Implements the **python/bokeh** version of `waterfall-basic`. **File:** `plots/waterfall-basic/implementations/python/bokeh.py` **Parent Issue:** #777 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25410718422)* --------- 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 4bc80d7 commit 781d3fe

2 files changed

Lines changed: 247 additions & 178 deletions

File tree

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,86 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
waterfall-basic: Basic Waterfall Chart
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-24
3+
Library: bokeh 3.9.0 | Python 3.13.13
4+
Quality: 86/100 | Updated: 2026-05-06
55
"""
66

7-
from bokeh.io import export_png
8-
from bokeh.models import ColumnDataSource, FactorRange, Label
9-
from bokeh.plotting import figure, save
7+
import os
8+
import time
9+
from pathlib import Path
1010

11+
from bokeh.io import output_file, save
12+
from bokeh.models import ColumnDataSource, FactorRange, HoverTool, Label
13+
from bokeh.plotting import figure
14+
from selenium import webdriver
15+
from selenium.webdriver.chrome.options import Options
1116

12-
# Data - quarterly financial breakdown from revenue to net income
17+
18+
THEME = os.getenv("ANYPLOT_THEME", "light")
19+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
20+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
21+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
22+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
23+
24+
# Okabe-Ito palette
25+
POSITIVE = "#009E73" # brand green for positive changes
26+
NEGATIVE = "#D55E00" # vermillion for negative changes
27+
TOTAL = "#0072B2" # blue for totals
28+
29+
# Data - quarterly financial breakdown
1330
categories = ["Starting Revenue", "Product Sales", "Services", "Refunds", "Operating Costs", "Marketing", "Net Income"]
14-
# First value is the starting total, last value will be calculated as final total
15-
# Middle values are changes (positive = increase, negative = decrease)
16-
changes = [150000, 50000, 35000, -8000, -75000, -22000, 0] # Last is placeholder for total
31+
changes = [150000, 50000, 35000, -8000, -75000, -22000, 0]
1732

1833
# Calculate waterfall positions
1934
running_total = 0
2035
bar_bottoms = []
2136
bar_tops = []
2237
bar_colors = []
23-
display_values = [] # For labels
38+
display_values = []
2439

2540
for i, (_cat, change) in enumerate(zip(categories, changes, strict=True)):
2641
if i == 0:
2742
# Starting total - full bar from 0
2843
running_total = change
2944
bar_bottoms.append(0)
3045
bar_tops.append(running_total)
31-
bar_colors.append("#306998") # Python Blue for totals
46+
bar_colors.append(TOTAL)
3247
display_values.append(running_total)
3348
elif i == len(categories) - 1:
3449
# Final total - full bar from 0 to current running total
3550
bar_bottoms.append(0)
3651
bar_tops.append(running_total)
37-
bar_colors.append("#306998") # Python Blue for totals
52+
bar_colors.append(TOTAL)
3853
display_values.append(running_total)
3954
else:
4055
# Intermediate changes
4156
if change >= 0:
4257
bar_bottoms.append(running_total)
4358
bar_tops.append(running_total + change)
44-
bar_colors.append("#2ECC71") # Green for positive
59+
bar_colors.append(POSITIVE)
4560
else:
4661
bar_bottoms.append(running_total + change)
4762
bar_tops.append(running_total)
48-
bar_colors.append("#E74C3C") # Red for negative
63+
bar_colors.append(NEGATIVE)
4964
running_total += change
5065
display_values.append(change)
5166

5267
# Create data source
53-
source = ColumnDataSource(data={"categories": categories, "bottom": bar_bottoms, "top": bar_tops, "color": bar_colors})
68+
source = ColumnDataSource(
69+
data={
70+
"categories": categories,
71+
"bottom": bar_bottoms,
72+
"top": bar_tops,
73+
"color": bar_colors,
74+
"display": [f"${v:,.0f}" for v in display_values],
75+
}
76+
)
5477

55-
# Create figure (4800 x 2700 px)
78+
# Create figure
5679
p = figure(
5780
x_range=FactorRange(*categories, range_padding=0.1),
5881
width=4800,
5982
height=2700,
60-
title="waterfall-basic · bokeh · pyplots.ai",
83+
title="waterfall-basic · bokeh · anyplot.ai",
6184
x_axis_label="Financial Category",
6285
y_axis_label="Amount ($)",
6386
toolbar_location=None,
@@ -71,11 +94,15 @@
7194
width=0.6,
7295
source=source,
7396
color="color",
74-
line_color="white",
97+
line_color=INK_SOFT,
7598
line_width=2,
7699
alpha=0.9,
77100
)
78101

102+
# Add HoverTool for interactivity
103+
hover = HoverTool(tooltips=[("Category", "@categories"), ("Amount", "@display")])
104+
p.add_tools(hover)
105+
79106
# Calculate running totals for connector lines
80107
running_totals = []
81108
rt = 0
@@ -86,35 +113,30 @@
86113
rt += change
87114
running_totals.append(rt)
88115

89-
# Draw connector lines between bars (showing running total flow)
90-
for i in range(len(categories) - 2): # Don't draw connector to final total bar
91-
# The connector should be at the running total level after bar i
116+
# Draw connector lines between bars
117+
for i in range(len(categories) - 2):
92118
connector_y = running_totals[i]
93-
94119
p.line(
95120
x=[categories[i], categories[i + 1]],
96121
y=[connector_y, connector_y],
97-
line_color="#7F8C8D",
122+
line_color=INK_SOFT,
98123
line_width=2,
99124
line_dash="dashed",
100-
alpha=0.7,
125+
alpha=0.5,
101126
)
102127

103128
# Add value labels on bars
104129
max_value = max(bar_tops)
105-
label_offset = max_value * 0.03 # 3% of max value for offset
130+
label_offset = max_value * 0.03
106131

107132
for i, (_cat, _bottom, top, display_val) in enumerate(
108133
zip(categories, bar_bottoms, bar_tops, display_values, strict=True)
109134
):
110-
# Label position - above the bar
111135
label_y = top + label_offset
112136

113137
if i == 0 or i == len(categories) - 1:
114-
# Total bars - show absolute value
115138
label_text = f"${display_val:,.0f}"
116139
else:
117-
# Change bars - show with sign
118140
if display_val >= 0:
119141
label_text = f"+${display_val:,.0f}"
120142
else:
@@ -127,33 +149,62 @@
127149
text_font_size="20pt",
128150
text_align="center",
129151
text_baseline="bottom",
130-
text_color="#2C3E50",
152+
text_color=INK,
131153
)
132154
p.add_layout(label)
133155

134156
# Style
135-
p.title.text_font_size = "32pt"
136-
p.xaxis.axis_label_text_font_size = "24pt"
137-
p.yaxis.axis_label_text_font_size = "24pt"
157+
p.title.text_font_size = "28pt"
158+
p.title.text_color = INK
159+
160+
p.xaxis.axis_label_text_font_size = "22pt"
161+
p.xaxis.axis_label_text_color = INK
162+
p.yaxis.axis_label_text_font_size = "22pt"
163+
p.yaxis.axis_label_text_color = INK
164+
138165
p.xaxis.major_label_text_font_size = "18pt"
139166
p.yaxis.major_label_text_font_size = "18pt"
140-
p.xaxis.major_label_orientation = 0.5 # Slight rotation for readability
167+
p.xaxis.major_label_text_color = INK_SOFT
168+
p.yaxis.major_label_text_color = INK_SOFT
169+
p.xaxis.major_label_orientation = 0.3
170+
171+
p.xaxis.axis_line_color = INK_SOFT
172+
p.yaxis.axis_line_color = INK_SOFT
173+
p.xaxis.major_tick_line_color = INK_SOFT
174+
p.yaxis.major_tick_line_color = INK_SOFT
141175

142-
# Grid styling
143176
p.xgrid.grid_line_color = None
144-
p.ygrid.grid_line_alpha = 0.3
145-
p.ygrid.grid_line_dash = "dashed"
177+
p.ygrid.grid_line_color = INK
178+
p.ygrid.grid_line_alpha = 0.1
146179

147-
# Background
148-
p.background_fill_color = "#FAFAFA"
149-
p.border_fill_color = "#FFFFFF"
180+
p.background_fill_color = PAGE_BG
181+
p.border_fill_color = PAGE_BG
182+
p.outline_line_color = INK_SOFT
150183

151-
# Axis styling
152-
p.yaxis.formatter.use_scientific = False
153184
p.y_range.start = 0
154-
p.y_range.end = max_value * 1.15 # 15% padding above max
155-
p.min_border_left = 80 # Extra left margin for y-axis labels
156-
157-
# Save as PNG and HTML
158-
export_png(p, filename="plot.png")
159-
save(p, filename="plot.html", title="waterfall-basic · bokeh · pyplots.ai")
185+
p.y_range.end = max_value * 1.15
186+
p.min_border_left = 80
187+
188+
# Save HTML
189+
output_file(f"plot-{THEME}.html")
190+
save(p)
191+
192+
# Screenshot with Selenium
193+
W, H = 4800, 2700
194+
opts = Options()
195+
for arg in (
196+
"--headless=new",
197+
"--no-sandbox",
198+
"--disable-dev-shm-usage",
199+
"--disable-gpu",
200+
f"--window-size={W},{H}",
201+
"--hide-scrollbars",
202+
):
203+
opts.add_argument(arg)
204+
205+
driver = webdriver.Chrome(options=opts)
206+
driver.set_window_size(W, H)
207+
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
208+
time.sleep(3)
209+
driver.save_screenshot(f"plot-{THEME}.png")
210+
driver.quit()

0 commit comments

Comments
 (0)