Skip to content

Commit 20f03c8

Browse files
feat(bokeh): implement gauge-basic (#5394)
## 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/24931133255)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent b758d79 commit 20f03c8

2 files changed

Lines changed: 263 additions & 228 deletions

File tree

Lines changed: 100 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,141 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: bokeh 3.9.0 | Python 3.14.4
4+
Quality: 87/100 | Updated: 2026-04-25
55
"""
66

7+
import os
8+
79
import numpy as np
810
from bokeh.io import export_png, output_file, save
911
from bokeh.models import Label
1012
from bokeh.plotting import figure
1113

1214

15+
# Theme tokens
16+
THEME = os.getenv("ANYPLOT_THEME", "light")
17+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
21+
22+
# Okabe-Ito zones: low / mid / high (intuitive + colorblind-safe)
23+
ZONE_LOW = "#D55E00" # vermillion
24+
ZONE_MID = "#E69F00" # orange
25+
ZONE_HIGH = "#009E73" # brand bluish green (Okabe-Ito position 1)
26+
1327
# Data
1428
value = 72
1529
min_value = 0
1630
max_value = 100
1731
thresholds = [30, 70]
1832

19-
# Gauge parameters
20-
start_angle = np.pi # 180 degrees (left side)
21-
end_angle = 0 # 0 degrees (right side)
22-
center_x = 0
23-
center_y = 0
24-
outer_radius = 0.9
25-
inner_radius = 0.6
26-
needle_length = 0.85
33+
# Gauge geometry
34+
center_x, center_y = 0.0, 0.0
35+
outer_radius = 0.95
36+
inner_radius = 0.62
37+
needle_length = 0.86
38+
start_angle = np.pi # left
39+
40+
# Map data values onto the semi-circle (pi → 0 radians)
41+
zone_bounds = np.array([min_value] + thresholds + [max_value])
42+
zone_angles = start_angle - (zone_bounds - min_value) / (max_value - min_value) * np.pi
43+
44+
tick_values = np.array([0, 25, 50, 75, 100])
45+
tick_angles = start_angle - (tick_values - min_value) / (max_value - min_value) * np.pi
46+
47+
needle_angle = start_angle - (value - min_value) / (max_value - min_value) * np.pi
2748

28-
# Create figure
49+
# Figure
2950
p = figure(
3051
width=4800,
3152
height=2700,
32-
title="gauge-basic · bokeh · pyplots.ai",
33-
x_range=(-1.2, 1.2),
34-
y_range=(-0.3, 1.2),
53+
title="gauge-basic · bokeh · anyplot.ai",
54+
x_range=(-1.25, 1.25),
55+
y_range=(-0.45, 1.20),
3556
tools="",
3657
toolbar_location=None,
58+
background_fill_color=PAGE_BG,
59+
border_fill_color=PAGE_BG,
60+
outline_line_color=None,
3761
)
38-
39-
# Remove axes and grid
4062
p.axis.visible = False
4163
p.grid.visible = False
42-
p.outline_line_color = None
4364

44-
# Title styling
45-
p.title.text_font_size = "36pt"
65+
p.title.text_font_size = "44pt"
66+
p.title.text_color = INK
4667
p.title.align = "center"
4768

48-
# Colors for zones (red, yellow, green)
49-
zone_colors = ["#E74C3C", "#FFD43B", "#27AE60"]
50-
51-
# Draw arc segments for each zone
52-
zones = [min_value] + thresholds + [max_value]
53-
for i in range(len(zones) - 1):
54-
zone_start = zones[i]
55-
zone_end = zones[i + 1]
56-
57-
# Convert value range to angle range
58-
angle_start = start_angle - (zone_start - min_value) / (max_value - min_value) * np.pi
59-
angle_end = start_angle - (zone_end - min_value) / (max_value - min_value) * np.pi
60-
61-
# Create wedge for this zone
62-
num_points = 50
63-
angles = np.linspace(angle_start, angle_end, num_points)
64-
65-
# Outer arc points
66-
outer_x = center_x + outer_radius * np.cos(angles)
67-
outer_y = center_y + outer_radius * np.sin(angles)
68-
69-
# Inner arc points (reversed for closed polygon)
70-
inner_x = center_x + inner_radius * np.cos(angles[::-1])
71-
inner_y = center_y + inner_radius * np.sin(angles[::-1])
72-
73-
# Combine to form closed polygon
74-
xs = np.concatenate([outer_x, inner_x])
75-
ys = np.concatenate([outer_y, inner_y])
76-
77-
p.patch(xs, ys, fill_color=zone_colors[i], line_color="white", line_width=2)
78-
79-
# Draw tick marks and labels
80-
tick_values = [0, 25, 50, 75, 100]
81-
for tick_val in tick_values:
82-
tick_angle = start_angle - (tick_val - min_value) / (max_value - min_value) * np.pi
83-
84-
# Tick line (outer)
85-
tick_outer_x = center_x + (outer_radius + 0.02) * np.cos(tick_angle)
86-
tick_outer_y = center_y + (outer_radius + 0.02) * np.sin(tick_angle)
87-
tick_inner_x = center_x + (outer_radius + 0.08) * np.cos(tick_angle)
88-
tick_inner_y = center_y + (outer_radius + 0.08) * np.sin(tick_angle)
89-
90-
p.line([tick_outer_x, tick_inner_x], [tick_outer_y, tick_inner_y], line_color="#2C3E50", line_width=4)
69+
# Zone arcs via annular_wedge (cleaner than manual polygons)
70+
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,
82+
)
9183

92-
# Tick label
93-
label_x = center_x + (outer_radius + 0.18) * np.cos(tick_angle)
94-
label_y = center_y + (outer_radius + 0.18) * np.sin(tick_angle)
84+
# Tick marks and labels
85+
for tick_val, a in zip(tick_values, tick_angles, strict=True):
86+
cos_a, sin_a = np.cos(a), np.sin(a)
9587

96-
label = Label(
97-
x=label_x,
98-
y=label_y,
99-
text=str(tick_val),
100-
text_font_size="24pt",
101-
text_color="#2C3E50",
102-
text_align="center",
103-
text_baseline="middle",
88+
p.line(
89+
[center_x + (outer_radius + 0.02) * cos_a, center_x + (outer_radius + 0.10) * cos_a],
90+
[center_y + (outer_radius + 0.02) * sin_a, center_y + (outer_radius + 0.10) * sin_a],
91+
line_color=INK_SOFT,
92+
line_width=4,
10493
)
105-
p.add_layout(label)
10694

107-
# Draw needle
108-
needle_angle = start_angle - (value - min_value) / (max_value - min_value) * np.pi
109-
needle_x = center_x + needle_length * np.cos(needle_angle)
110-
needle_y = center_y + needle_length * np.sin(needle_angle)
95+
p.add_layout(
96+
Label(
97+
x=center_x + (outer_radius + 0.20) * cos_a,
98+
y=center_y + (outer_radius + 0.20) * sin_a,
99+
text=str(tick_val),
100+
text_font_size="30pt",
101+
text_color=INK_SOFT,
102+
text_align="center",
103+
text_baseline="middle",
104+
)
105+
)
111106

112-
# Needle triangle
113-
needle_width = 0.04
114-
perp_angle = needle_angle + np.pi / 2
115-
base_x1 = center_x + needle_width * np.cos(perp_angle)
116-
base_y1 = center_y + needle_width * np.sin(perp_angle)
117-
base_x2 = center_x - needle_width * np.cos(perp_angle)
118-
base_y2 = center_y - needle_width * np.sin(perp_angle)
107+
# Needle (triangle)
108+
needle_tip_x = center_x + needle_length * np.cos(needle_angle)
109+
needle_tip_y = center_y + needle_length * np.sin(needle_angle)
110+
half_base = 0.035
111+
perp = needle_angle + np.pi / 2
112+
base1_x = center_x + half_base * np.cos(perp)
113+
base1_y = center_y + half_base * np.sin(perp)
114+
base2_x = center_x - half_base * np.cos(perp)
115+
base2_y = center_y - half_base * np.sin(perp)
119116

120117
p.patch(
121-
[base_x1, needle_x, base_x2], [base_y1, needle_y, base_y2], fill_color="#306998", line_color="#1A3A5C", line_width=2
118+
[base1_x, needle_tip_x, base2_x], [base1_y, needle_tip_y, base2_y], fill_color=INK, line_color=INK, line_width=2
122119
)
123120

124-
# Center circle
125-
p.circle(center_x, center_y, radius=0.08, fill_color="#306998", line_color="#1A3A5C", line_width=3)
121+
# 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)
126123

127124
# Value display
128-
value_label = Label(
129-
x=center_x,
130-
y=-0.18,
131-
text=str(value),
132-
text_font_size="48pt",
133-
text_color="#306998",
134-
text_align="center",
135-
text_baseline="middle",
136-
text_font_style="bold",
125+
p.add_layout(
126+
Label(
127+
x=center_x,
128+
y=-0.28,
129+
text=str(value),
130+
text_font_size="84pt",
131+
text_color=INK,
132+
text_align="center",
133+
text_baseline="middle",
134+
text_font_style="bold",
135+
)
137136
)
138-
p.add_layout(value_label)
139-
140-
# Save as PNG
141-
export_png(p, filename="plot.png")
142137

143-
# Save as HTML for interactivity
144-
output_file("plot.html")
138+
# Save
139+
export_png(p, filename=f"plot-{THEME}.png")
140+
output_file(f"plot-{THEME}.html")
145141
save(p)

0 commit comments

Comments
 (0)