Skip to content

Commit 42fcad8

Browse files
feat(bokeh): implement gauge-basic (#9534)
## Implementation: `gauge-basic` - python/bokeh Implements the **python/bokeh** version of `gauge-basic`. **File:** `plots/gauge-basic/implementations/python/bokeh.py` **Parent Issue:** #857 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28476691727)* --------- 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 b4653b8 commit 42fcad8

2 files changed

Lines changed: 226 additions & 119 deletions

File tree

Lines changed: 137 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: bokeh 3.9.0 | Python 3.14.4
4-
Quality: 87/100 | Updated: 2026-04-25
3+
Library: bokeh 3.9.1 | Python 3.13.14
4+
Quality: 89/100 | Updated: 2026-06-30
55
"""
66

77
import os
8+
import sys
9+
import time
10+
from pathlib import Path
11+
12+
13+
# Remove script's own directory from sys.path to prevent self-shadowing
14+
# (this file is named bokeh.py; without this, `import bokeh` would find itself)
15+
_here = os.path.dirname(os.path.abspath(__file__))
16+
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _here]
817

918
import numpy as np
10-
from bokeh.io import export_png, output_file, save
11-
from bokeh.models import Label
19+
from bokeh.io import output_file, save
20+
from bokeh.models import ColumnDataSource, HoverTool, Label
1221
from bokeh.plotting import figure
22+
from selenium import webdriver
23+
from selenium.webdriver.chrome.options import Options
1324

1425

1526
# Theme tokens
@@ -19,12 +30,12 @@
1930
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
2031
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2132

22-
# Okabe-Ito zones: low / mid / high (intuitive + colorblind-safe)
23-
ZONE_LOW = "#AE3030" # imprint red — bad
24-
ZONE_MID = "#DDCC77" # imprint amber — caution
25-
ZONE_HIGH = "#009E73" # imprint green — good
33+
# Imprint zone colors — semantic convention: red=bad, amber=caution, green=good
34+
ZONE_LOW = "#AE3030" # Imprint matte red
35+
ZONE_MID = "#DDCC77" # Imprint amber
36+
ZONE_HIGH = "#009E73" # Imprint brand green
2637

27-
# Data
38+
# Data — CPU utilization
2839
value = 72
2940
min_value = 0
3041
max_value = 100
@@ -34,8 +45,9 @@
3445
center_x, center_y = 0.0, 0.0
3546
outer_radius = 0.95
3647
inner_radius = 0.62
48+
arc_mid_radius = (inner_radius + outer_radius) / 2 # label midpoint inside arc
3749
needle_length = 0.86
38-
start_angle = np.pi # left
50+
start_angle = np.pi # leftmost position (180°)
3951

4052
# Map data values onto the semi-circle (pi → 0 radians)
4153
zone_bounds = np.array([min_value] + thresholds + [max_value])
@@ -46,39 +58,86 @@
4658

4759
needle_angle = start_angle - (value - min_value) / (max_value - min_value) * np.pi
4860

49-
# Figure
61+
# Figure — landscape canvas; toolbar_location=None prevents height bloat in screenshot
62+
W, H = 3200, 1800
5063
p = figure(
51-
width=4800,
52-
height=2700,
53-
title="gauge-basic · bokeh · anyplot.ai",
54-
x_range=(-1.25, 1.25),
55-
y_range=(-0.45, 1.20),
64+
width=W,
65+
height=H,
66+
title="gauge-basic · python · bokeh · anyplot.ai",
67+
x_range=(-1.5, 1.5),
68+
y_range=(-0.58, 1.32),
5669
tools="",
5770
toolbar_location=None,
5871
background_fill_color=PAGE_BG,
5972
border_fill_color=PAGE_BG,
6073
outline_line_color=None,
74+
min_border_bottom=80,
75+
min_border_left=80,
76+
min_border_top=120,
77+
min_border_right=80,
6178
)
6279
p.axis.visible = False
6380
p.grid.visible = False
81+
p.outline_line_color = None
6482

65-
p.title.text_font_size = "44pt"
83+
p.title.text_font_size = "50pt"
6684
p.title.text_color = INK
6785
p.title.align = "center"
6886

69-
# Zone arcs via annular_wedge (cleaner than manual polygons)
87+
# Zone arcs via annular_wedge — ColumnDataSource enables HoverTool
7088
zone_colors = [ZONE_LOW, ZONE_MID, ZONE_HIGH]
71-
for i, color in enumerate(zone_colors):
72-
p.annular_wedge(
73-
x=center_x,
74-
y=center_y,
75-
inner_radius=inner_radius,
76-
outer_radius=outer_radius,
77-
start_angle=zone_angles[i + 1],
78-
end_angle=zone_angles[i],
79-
fill_color=color,
80-
line_color=PAGE_BG,
81-
line_width=4,
89+
zone_names = ["Low", "Caution", "Optimal"]
90+
zone_ranges = ["0–30", "30–70", "70–100"]
91+
92+
zone_source = ColumnDataSource(
93+
data={
94+
"x": [center_x, center_x, center_x],
95+
"y": [center_y, center_y, center_y],
96+
"inner_radius": [inner_radius, inner_radius, inner_radius],
97+
"outer_radius": [outer_radius, outer_radius, outer_radius],
98+
"start_angle": [float(zone_angles[1]), float(zone_angles[2]), float(zone_angles[3])],
99+
"end_angle": [float(zone_angles[0]), float(zone_angles[1]), float(zone_angles[2])],
100+
"fill_color": zone_colors,
101+
"zone": zone_names,
102+
"range": zone_ranges,
103+
}
104+
)
105+
106+
zone_renderer = p.annular_wedge(
107+
x="x",
108+
y="y",
109+
inner_radius="inner_radius",
110+
outer_radius="outer_radius",
111+
start_angle="start_angle",
112+
end_angle="end_angle",
113+
fill_color="fill_color",
114+
line_color=PAGE_BG,
115+
line_width=4,
116+
source=zone_source,
117+
)
118+
119+
p.add_tools(HoverTool(renderers=[zone_renderer], tooltips=[("Zone", "@zone"), ("Range", "@range")]))
120+
121+
# Zone labels inside each arc segment — contrasting fixed colors (zones are theme-invariant)
122+
# red/green zones get light text; amber zone gets dark text for contrast
123+
zone_mid_values = [15.0, 50.0, 85.0]
124+
zone_label_colors = ["#FFFDF6", "#1A1A17", "#FFFDF6"]
125+
126+
for mid_val, label_text, label_color in zip(zone_mid_values, zone_names, zone_label_colors, strict=True):
127+
a = start_angle - (mid_val / max_value) * np.pi
128+
lx = arc_mid_radius * np.cos(a)
129+
ly = arc_mid_radius * np.sin(a)
130+
p.add_layout(
131+
Label(
132+
x=lx,
133+
y=ly,
134+
text=label_text,
135+
text_font_size="22pt",
136+
text_color=label_color,
137+
text_align="center",
138+
text_baseline="middle",
139+
angle=float(a - np.pi / 2), # tangent along arc for natural text flow
140+
)
82141
)
83142

84143
# Tick marks and labels
@@ -94,17 +153,17 @@
94153

95154
p.add_layout(
96155
Label(
97-
x=center_x + (outer_radius + 0.20) * cos_a,
98-
y=center_y + (outer_radius + 0.20) * sin_a,
156+
x=center_x + (outer_radius + 0.22) * cos_a,
157+
y=center_y + (outer_radius + 0.22) * sin_a,
99158
text=str(tick_val),
100-
text_font_size="30pt",
159+
text_font_size="31pt",
101160
text_color=INK_SOFT,
102161
text_align="center",
103162
text_baseline="middle",
104163
)
105164
)
106165

107-
# Needle (triangle)
166+
# Needle (triangle pointing to current value)
108167
needle_tip_x = center_x + needle_length * np.cos(needle_angle)
109168
needle_tip_y = center_y + needle_length * np.sin(needle_angle)
110169
half_base = 0.035
@@ -119,23 +178,62 @@
119178
)
120179

121180
# Center hub
122-
p.scatter(x=[center_x], y=[center_y], size=70, marker="circle", fill_color=INK, line_color=PAGE_BG, line_width=4)
181+
p.scatter(x=[center_x], y=[center_y], size=55, marker="circle", fill_color=INK, line_color=PAGE_BG, line_width=4)
123182

124183
# Value display
125184
p.add_layout(
126185
Label(
127186
x=center_x,
128-
y=-0.28,
187+
y=-0.20,
129188
text=str(value),
130-
text_font_size="84pt",
189+
text_font_size="80pt",
131190
text_color=INK,
132191
text_align="center",
133192
text_baseline="middle",
134193
text_font_style="bold",
135194
)
136195
)
137196

138-
# Save
139-
export_png(p, filename=f"plot-{THEME}.png")
140-
output_file(f"plot-{THEME}.html")
197+
# Metric label
198+
p.add_layout(
199+
Label(
200+
x=center_x,
201+
y=-0.44,
202+
text="CPU Utilization (%)",
203+
text_font_size="30pt",
204+
text_color=INK_SOFT,
205+
text_align="center",
206+
text_baseline="middle",
207+
)
208+
)
209+
210+
# Save interactive HTML
211+
html_path = Path(f"plot-{THEME}.html")
212+
output_file(str(html_path))
141213
save(p)
214+
215+
# Inject body background CSS to prevent thin border artifact in headless-Chrome screenshot
216+
html_content = html_path.read_text()
217+
body_style = f"<style>body{{margin:0;padding:0;background:{PAGE_BG};}}</style>"
218+
html_content = html_content.replace("</head>", f"{body_style}\n</head>", 1)
219+
html_path.write_text(html_content)
220+
221+
# Screenshot via headless Chrome — use CDP to set exact viewport to match figure dimensions
222+
opts = Options()
223+
for arg in (
224+
"--headless=new",
225+
"--no-sandbox",
226+
"--disable-dev-shm-usage",
227+
"--disable-gpu",
228+
f"--window-size={W},{H}",
229+
"--hide-scrollbars",
230+
):
231+
opts.add_argument(arg)
232+
driver = webdriver.Chrome(options=opts)
233+
driver.execute_cdp_cmd(
234+
"Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
235+
)
236+
driver.get(f"file://{html_path.resolve()}")
237+
time.sleep(3)
238+
driver.save_screenshot(f"plot-{THEME}.png")
239+
driver.quit()

0 commit comments

Comments
 (0)