Skip to content

Commit 8c24003

Browse files
feat(plotnine): implement gauge-basic (#5395)
## Implementation: `gauge-basic` - python/plotnine Implements the **python/plotnine** version of `gauge-basic`. **File:** `plots/gauge-basic/implementations/python/plotnine.py` **Parent Issue:** #857 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24931308844)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 20f03c8 commit 8c24003

2 files changed

Lines changed: 344 additions & 85 deletions

File tree

Lines changed: 107 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: plotnine 0.15.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-14
3+
Library: plotnine 0.15.3 | Python 3.14.4
4+
Quality: 86/100 | Created: 2026-04-25
55
"""
66

7-
import numpy as np
8-
import pandas as pd
9-
from plotnine import (
7+
import os
8+
import sys
9+
10+
11+
# Avoid name collision: drop this script's directory from sys.path
12+
# so `from plotnine import ...` resolves to the installed package.
13+
_HERE = os.path.dirname(os.path.abspath(__file__))
14+
sys.path = [p for p in sys.path if os.path.abspath(p) != _HERE]
15+
16+
import numpy as np # noqa: E402
17+
import pandas as pd # noqa: E402
18+
from plotnine import ( # noqa: E402
1019
aes,
1120
coord_fixed,
1221
element_blank,
22+
element_rect,
1323
geom_point,
1424
geom_polygon,
1525
geom_segment,
@@ -22,106 +32,131 @@
2232
)
2333

2434

35+
# Theme tokens
36+
THEME = os.getenv("ANYPLOT_THEME", "light")
37+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
38+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
39+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
40+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
41+
42+
# Okabe-Ito zone colors (colorblind-safe red/yellow/green)
43+
ZONE_BAD = "#D55E00" # vermillion
44+
ZONE_WARN = "#E69F00" # orange
45+
ZONE_GOOD = "#009E73" # bluish green (brand)
46+
2547
# Data
2648
value = 72
2749
min_value = 0
2850
max_value = 100
2951
thresholds = [30, 70]
3052

31-
# Gauge parameters
32-
inner_radius = 0.5
53+
# Geometry
54+
inner_radius = 0.7
3355
outer_radius = 1.0
34-
start_angle = np.pi # 180 degrees (left)
35-
end_angle = 0 # 0 degrees (right)
36-
37-
# Create color zones based on thresholds
38-
zones = []
39-
zone_colors = ["#E74C3C", "#F1C40F", "#27AE60"] # Red, Yellow, Green
40-
zone_bounds = [min_value] + thresholds + [max_value]
41-
42-
for i in range(len(zone_bounds) - 1):
56+
start_angle = np.pi
57+
end_angle = 0.0
58+
59+
# Zone arc polygons
60+
zone_colors = [ZONE_BAD, ZONE_WARN, ZONE_GOOD]
61+
zone_bounds = [min_value, *thresholds, max_value]
62+
zone_records = []
63+
for i in range(len(zone_colors)):
4364
start_pct = (zone_bounds[i] - min_value) / (max_value - min_value)
4465
end_pct = (zone_bounds[i + 1] - min_value) / (max_value - min_value)
45-
46-
# Create arc segment as polygon
4766
start_ang = start_angle - start_pct * (start_angle - end_angle)
4867
end_ang = start_angle - end_pct * (start_angle - end_angle)
49-
n_points = 50
50-
68+
n_points = 60
5169
angles_outer = np.linspace(start_ang, end_ang, n_points)
5270
angles_inner = np.linspace(end_ang, start_ang, n_points)
71+
xs = np.concatenate([outer_radius * np.cos(angles_outer), inner_radius * np.cos(angles_inner)])
72+
ys = np.concatenate([outer_radius * np.sin(angles_outer), inner_radius * np.sin(angles_inner)])
73+
for j in range(len(xs)):
74+
zone_records.append({"x": xs[j], "y": ys[j], "zone": str(i)})
5375

54-
x = np.concatenate([outer_radius * np.cos(angles_outer), inner_radius * np.cos(angles_inner)])
55-
y = np.concatenate([outer_radius * np.sin(angles_outer), inner_radius * np.sin(angles_inner)])
56-
57-
for j in range(len(x)):
58-
zones.append({"x": x[j], "y": y[j], "zone": i, "color": zone_colors[i]})
59-
60-
df_zones = pd.DataFrame(zones)
76+
df_zones = pd.DataFrame(zone_records)
6177

62-
# Create needle pointing to value
78+
# Needle pointing to current value
6379
value_pct = (value - min_value) / (max_value - min_value)
6480
needle_angle = start_angle - value_pct * (start_angle - end_angle)
65-
needle_length = outer_radius * 0.85
66-
81+
needle_length = inner_radius * 0.92
6782
df_needle = pd.DataFrame(
6883
{"x": [0], "y": [0], "xend": [needle_length * np.cos(needle_angle)], "yend": [needle_length * np.sin(needle_angle)]}
6984
)
7085

71-
# Create tick marks and labels
72-
tick_values = [0, 25, 50, 75, 100]
73-
tick_data = []
74-
label_data = []
75-
tick_inner = outer_radius * 1.02
76-
tick_outer = outer_radius * 1.08
77-
label_radius = outer_radius * 1.18
86+
# Tick marks and labels
87+
major_ticks = [0, 25, 50, 75, 100]
88+
minor_ticks = [t for t in range(0, 101, 5) if t not in major_ticks]
89+
90+
minor_tick_records = []
91+
for tv in minor_ticks:
92+
pct = (tv - min_value) / (max_value - min_value)
93+
ang = start_angle - pct * (start_angle - end_angle)
94+
minor_tick_records.append(
95+
{
96+
"x": (outer_radius * 1.02) * np.cos(ang),
97+
"y": (outer_radius * 1.02) * np.sin(ang),
98+
"xend": (outer_radius * 1.05) * np.cos(ang),
99+
"yend": (outer_radius * 1.05) * np.sin(ang),
100+
}
101+
)
78102

79-
for tv in tick_values:
103+
major_tick_records = []
104+
label_records = []
105+
for tv in major_ticks:
80106
pct = (tv - min_value) / (max_value - min_value)
81107
ang = start_angle - pct * (start_angle - end_angle)
82-
tick_data.append(
108+
major_tick_records.append(
83109
{
84-
"x": tick_inner * np.cos(ang),
85-
"y": tick_inner * np.sin(ang),
86-
"xend": tick_outer * np.cos(ang),
87-
"yend": tick_outer * np.sin(ang),
110+
"x": (outer_radius * 1.02) * np.cos(ang),
111+
"y": (outer_radius * 1.02) * np.sin(ang),
112+
"xend": (outer_radius * 1.10) * np.cos(ang),
113+
"yend": (outer_radius * 1.10) * np.sin(ang),
88114
}
89115
)
90-
label_data.append({"x": label_radius * np.cos(ang), "y": label_radius * np.sin(ang), "label": str(tv)})
116+
label_records.append(
117+
{"x": (outer_radius * 1.20) * np.cos(ang), "y": (outer_radius * 1.20) * np.sin(ang), "label": str(tv)}
118+
)
91119

92-
df_ticks = pd.DataFrame(tick_data)
93-
df_labels = pd.DataFrame(label_data)
120+
df_minor_ticks = pd.DataFrame(minor_tick_records)
121+
df_major_ticks = pd.DataFrame(major_tick_records)
122+
df_labels = pd.DataFrame(label_records)
94123

95-
# Value display label
96-
df_value = pd.DataFrame({"x": [0], "y": [-0.2], "label": [str(value)]})
124+
# Center cap (two layered points for definition)
125+
df_cap_outer = pd.DataFrame({"x": [0], "y": [0]})
126+
df_cap_inner = pd.DataFrame({"x": [0], "y": [0]})
97127

98-
# Title label
99-
df_title = pd.DataFrame({"x": [0], "y": [1.45], "label": ["gauge-basic · plotnine · pyplots.ai"]})
128+
# Value display and context
129+
df_value = pd.DataFrame({"x": [0], "y": [-0.30], "label": [str(value)]})
130+
df_context = pd.DataFrame({"x": [0], "y": [-0.55], "label": ["Current Sales"]})
100131

101-
# Build the plot
132+
# Title
133+
df_title = pd.DataFrame({"x": [0], "y": [1.42], "label": ["gauge-basic · plotnine · anyplot.ai"]})
134+
135+
# Build plot
102136
plot = (
103137
ggplot()
104-
# Draw zone arcs as polygons grouped by zone
105-
+ geom_polygon(aes(x="x", y="y", fill="factor(zone)", group="zone"), data=df_zones, color="white", size=0.5)
106-
# Draw tick marks
107-
+ geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_ticks, color="#2C3E50", size=1.5)
108-
# Draw tick labels
109-
+ geom_text(aes(x="x", y="y", label="label"), data=df_labels, color="#2C3E50", size=16)
110-
# Draw needle
111-
+ geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_needle, color="#2C3E50", size=3)
112-
# Draw needle center circle
113-
+ geom_point(aes(x="x", y="y"), data=pd.DataFrame({"x": [0], "y": [0]}), color="#2C3E50", size=8)
114-
# Draw value label
115-
+ geom_text(aes(x="x", y="y", label="label"), data=df_value, color="#2C3E50", size=24, fontweight="bold")
116-
# Draw title
117-
+ geom_text(aes(x="x", y="y", label="label"), data=df_title, color="#2C3E50", size=14)
118-
+ coord_fixed(ratio=1, xlim=(-1.5, 1.5), ylim=(-0.5, 1.6))
138+
+ geom_polygon(aes(x="x", y="y", fill="zone", group="zone"), data=df_zones, color=PAGE_BG, size=1.2)
139+
+ geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_minor_ticks, color=INK_SOFT, size=0.8)
140+
+ geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_major_ticks, color=INK, size=1.6)
141+
+ geom_text(aes(x="x", y="y", label="label"), data=df_labels, color=INK_SOFT, size=18, fontweight="bold")
142+
+ geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_needle, color=INK, size=4, lineend="round")
143+
+ geom_point(aes(x="x", y="y"), data=df_cap_outer, color=INK, size=14)
144+
+ geom_point(aes(x="x", y="y"), data=df_cap_inner, color=PAGE_BG, size=5)
145+
+ geom_text(aes(x="x", y="y", label="label"), data=df_value, color=ZONE_GOOD, size=56, fontweight="bold")
146+
+ geom_text(aes(x="x", y="y", label="label"), data=df_context, color=INK_MUTED, size=20)
147+
+ geom_text(aes(x="x", y="y", label="label"), data=df_title, color=INK, size=24, fontweight="medium")
148+
+ scale_fill_manual(values=zone_colors, guide=None)
149+
+ coord_fixed(ratio=1, xlim=(-1.5, 1.5), ylim=(-0.75, 1.55))
119150
+ labs(x="", y="")
120151
+ theme_void()
121-
+ theme(figure_size=(16, 9), legend_position="none", plot_background=element_blank())
122-
# Manual fill colors for zones
123-
+ scale_fill_manual(values=zone_colors)
152+
+ theme(
153+
figure_size=(16, 9),
154+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
155+
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
156+
legend_position="none",
157+
axis_text=element_blank(),
158+
axis_title=element_blank(),
159+
)
124160
)
125161

126-
# Save
127-
plot.save("plot.png", dpi=300, verbose=False)
162+
plot.save(f"plot-{THEME}.png", dpi=300, verbose=False)

0 commit comments

Comments
 (0)