Skip to content

Commit d7a1bb0

Browse files
feat(altair): implement gauge-basic (#5396)
## Implementation: `gauge-basic` - python/altair Implements the **python/altair** version of `gauge-basic`. **File:** `plots/gauge-basic/implementations/python/altair.py` **Parent Issue:** #857 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24931340754)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 29c244c commit d7a1bb0

2 files changed

Lines changed: 253 additions & 199 deletions

File tree

Lines changed: 81 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,135 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: altair 6.1.0 | Python 3.14.4
4+
Quality: 88/100 | Updated: 2026-04-25
55
"""
66

7+
import os
8+
79
import altair as alt
810
import numpy as np
911
import pandas as pd
1012

1113

12-
# Data - Sales performance gauge
14+
# Theme tokens
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
21+
22+
# Okabe-Ito zone colors (colorblind-safe red/yellow/green)
23+
ZONE_BAD = "#D55E00" # vermillion
24+
ZONE_WARN = "#E69F00" # orange
25+
ZONE_GOOD = "#009E73" # bluish green (brand)
26+
27+
# Data — Sales performance gauge
1328
value = 72
1429
min_value = 0
1530
max_value = 100
1631
thresholds = [30, 70]
1732

33+
# Geometry: semi-circle from -pi/2 (left) to +pi/2 (right)
34+
boundaries = np.array([min_value] + thresholds + [max_value], dtype=float)
35+
boundary_angles = -np.pi / 2 + (boundaries - min_value) / (max_value - min_value) * np.pi
36+
needle_angle = -np.pi / 2 + (value - min_value) / (max_value - min_value) * np.pi
1837

19-
# Calculate angles for semi-circular gauge (180 degrees)
20-
# Map min_value to -pi/2 (left, 9 o'clock) and max_value to pi/2 (right, 3 o'clock)
21-
def value_to_angle(v):
22-
ratio = (v - min_value) / (max_value - min_value)
23-
return -np.pi / 2 + ratio * np.pi
24-
25-
26-
# Create arc segments for color zones
27-
zones = []
28-
zone_colors = ["#E53935", "#FFD43B", "#4CAF50"] # Red, Yellow, Green
29-
boundaries = [min_value] + thresholds + [max_value]
30-
31-
for i in range(len(boundaries) - 1):
32-
start_angle = value_to_angle(boundaries[i])
33-
end_angle = value_to_angle(boundaries[i + 1])
34-
zones.append(
35-
{
36-
"zone": i,
37-
"start": boundaries[i],
38-
"end": boundaries[i + 1],
39-
"startAngle": start_angle,
40-
"endAngle": end_angle,
41-
"color": zone_colors[i],
42-
}
43-
)
44-
45-
zones_df = pd.DataFrame(zones)
38+
# Zone arcs
39+
zones_df = pd.DataFrame(
40+
{"startAngle": boundary_angles[:-1], "endAngle": boundary_angles[1:], "color": [ZONE_BAD, ZONE_WARN, ZONE_GOOD]}
41+
)
4642

47-
# Create the gauge background arcs
4843
gauge_arcs = (
4944
alt.Chart(zones_df)
50-
.mark_arc(innerRadius=220, outerRadius=360, cornerRadius=6)
45+
.mark_arc(innerRadius=220, outerRadius=360, cornerRadius=6, stroke=PAGE_BG, strokeWidth=4)
5146
.encode(
5247
theta=alt.Theta("startAngle:Q", scale=None),
5348
theta2="endAngle:Q",
5449
color=alt.Color("color:N", scale=None, legend=None),
5550
)
5651
)
5752

58-
# Create needle indicator
59-
needle_angle = value_to_angle(value)
53+
# Needle
6054
needle_length = 300
61-
needle_x = needle_length * np.sin(needle_angle)
62-
needle_y = needle_length * np.cos(needle_angle)
63-
64-
needle_df = pd.DataFrame([{"x": 0, "y": 0, "x2": needle_x, "y2": needle_y}])
65-
55+
needle_df = pd.DataFrame(
56+
[{"x": 0.0, "y": 0.0, "x2": needle_length * np.sin(needle_angle), "y2": needle_length * np.cos(needle_angle)}]
57+
)
6658
needle = (
6759
alt.Chart(needle_df)
68-
.mark_rule(color="#306998", strokeWidth=10, strokeCap="round")
60+
.mark_rule(color=INK, strokeWidth=10, strokeCap="round")
6961
.encode(
70-
x=alt.X("x:Q", scale=alt.Scale(domain=[-500, 500]), axis=None),
71-
y=alt.Y("y:Q", scale=alt.Scale(domain=[-200, 500]), axis=None),
62+
x=alt.X("x:Q", scale=alt.Scale(domain=[-450, 450]), axis=None),
63+
y=alt.Y("y:Q", scale=alt.Scale(domain=[-260, 440]), axis=None),
7264
x2="x2:Q",
7365
y2="y2:Q",
7466
)
7567
)
7668

77-
# Center hub circle
69+
# Center hub (two-tone for definition)
7870
hub_df = pd.DataFrame([{"x": 0, "y": 0}])
79-
hub = alt.Chart(hub_df).mark_circle(size=2000, color="#306998").encode(x="x:Q", y="y:Q")
71+
hub_outer = alt.Chart(hub_df).mark_circle(size=2400, color=INK).encode(x="x:Q", y="y:Q")
72+
hub_inner = alt.Chart(hub_df).mark_circle(size=400, color=PAGE_BG).encode(x="x:Q", y="y:Q")
8073

81-
# Value label with unit
82-
value_label_df = pd.DataFrame([{"x": 0, "y": -120, "text": f"{value}%"}])
74+
# Prominent value label (centered below hub)
75+
value_label_df = pd.DataFrame([{"x": 0, "y": -140, "text": f"{value}"}])
8376
value_label = (
8477
alt.Chart(value_label_df)
85-
.mark_text(fontSize=80, fontWeight="bold", color="#306998")
78+
.mark_text(fontSize=88, fontWeight="bold", color=ZONE_GOOD, baseline="middle")
8679
.encode(x="x:Q", y="y:Q", text="text:N")
8780
)
8881

89-
# Min and max labels positioned at arc ends
90-
min_label_x = 400 * np.sin(value_to_angle(min_value))
91-
min_label_y = 400 * np.cos(value_to_angle(min_value))
92-
max_label_x = 400 * np.sin(value_to_angle(max_value))
93-
max_label_y = 400 * np.cos(value_to_angle(max_value))
82+
context_label_df = pd.DataFrame([{"x": 0, "y": -220, "text": "Current Sales"}])
83+
context_label = (
84+
alt.Chart(context_label_df)
85+
.mark_text(fontSize=22, color=INK_MUTED, baseline="middle")
86+
.encode(x="x:Q", y="y:Q", text="text:N")
87+
)
9488

95-
labels_df = pd.DataFrame(
89+
# Min and max labels just below the arc ends
90+
range_label_radius = 290
91+
range_labels_df = pd.DataFrame(
9692
[
97-
{"x": min_label_x - 50, "y": min_label_y, "text": str(min_value)},
98-
{"x": max_label_x + 50, "y": max_label_y, "text": str(max_value)},
93+
{"x": range_label_radius * np.sin(boundary_angles[0]), "y": -50, "text": str(min_value)},
94+
{"x": range_label_radius * np.sin(boundary_angles[-1]), "y": -50, "text": str(max_value)},
9995
]
10096
)
101-
range_labels = alt.Chart(labels_df).mark_text(fontSize=36, color="#555555").encode(x="x:Q", y="y:Q", text="text:N")
102-
103-
# Threshold labels on the arc
104-
threshold_labels_data = []
105-
for t in thresholds:
106-
angle = value_to_angle(t)
107-
label_radius = 420
108-
threshold_labels_data.append({"x": label_radius * np.sin(angle), "y": label_radius * np.cos(angle), "text": str(t)})
97+
range_labels = (
98+
alt.Chart(range_labels_df)
99+
.mark_text(fontSize=26, color=INK_SOFT, fontWeight="bold")
100+
.encode(x="x:Q", y="y:Q", text="text:N")
101+
)
109102

110-
threshold_labels_df = pd.DataFrame(threshold_labels_data)
103+
# Threshold labels above the arc
104+
threshold_angles = -np.pi / 2 + (np.array(thresholds, dtype=float) - min_value) / (max_value - min_value) * np.pi
105+
threshold_label_radius = 400
106+
threshold_labels_df = pd.DataFrame(
107+
{
108+
"x": threshold_label_radius * np.sin(threshold_angles),
109+
"y": threshold_label_radius * np.cos(threshold_angles),
110+
"text": [str(t) for t in thresholds],
111+
}
112+
)
111113
threshold_labels = (
112-
alt.Chart(threshold_labels_df).mark_text(fontSize=32, color="#555555").encode(x="x:Q", y="y:Q", text="text:N")
114+
alt.Chart(threshold_labels_df)
115+
.mark_text(fontSize=24, color=INK_SOFT, fontWeight="bold", dy=-10)
116+
.encode(x="x:Q", y="y:Q", text="text:N")
113117
)
114118

115-
# Combine all layers
119+
# Compose layers
116120
chart = (
117-
alt.layer(gauge_arcs, needle, hub, value_label, range_labels, threshold_labels)
121+
alt.layer(gauge_arcs, needle, hub_outer, hub_inner, threshold_labels, range_labels, value_label, context_label)
118122
.properties(
119-
width=1600, height=900, title=alt.Title("gauge-basic · altair · pyplots.ai", fontSize=48, anchor="middle")
123+
width=1600,
124+
height=900,
125+
background=PAGE_BG,
126+
title=alt.Title(
127+
"gauge-basic · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK, fontWeight="normal"
128+
),
120129
)
121-
.configure_view(strokeWidth=0)
130+
.configure_view(strokeWidth=0, fill=PAGE_BG)
122131
)
123132

124133
# Save
125-
chart.save("plot.png", scale_factor=3.0)
126-
chart.save("plot.html")
134+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
135+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)