Skip to content

Commit 9349ccb

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

2 files changed

Lines changed: 131 additions & 108 deletions

File tree

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
""" anyplot.ai
22
gauge-basic: Basic Gauge Chart
3-
Library: matplotlib 3.10.9 | Python 3.14.4
4-
Quality: 91/100 | Updated: 2026-04-25
3+
Library: matplotlib 3.11.0 | Python 3.13.14
4+
Quality: 91/100 | Updated: 2026-06-30
55
"""
66

77
import os
8+
import sys
9+
10+
11+
# Script named matplotlib.py shadows the installed package when run from its own directory.
12+
# Removing the first sys.path entry (the script's directory) before any matplotlib import
13+
# restores the normal package lookup — the venv site-packages takes over.
14+
sys.path.pop(0)
815

916
import matplotlib.patches as mpatches
1017
import matplotlib.pyplot as plt
@@ -14,34 +21,34 @@
1421
# Theme tokens
1522
THEME = os.getenv("ANYPLOT_THEME", "light")
1623
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17-
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
1824
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1925
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2026
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
2127

22-
# imprint semantic anchors (red / amber / green traffic-light)
28+
# Imprint semantic anchors (red / amber / green traffic-light)
2329
ZONE_BAD = "#AE3030" # matte red — semantic bad
2430
ZONE_WARN = "#DDCC77" # amber — semantic warning
2531
ZONE_GOOD = "#009E73" # brand green — semantic good
2632

2733
# Data
28-
value = 72 # Current sales value
34+
value = 72
2935
min_value = 0
3036
max_value = 100
31-
thresholds = [30, 70] # Boundaries for bad/warn/good zones
37+
thresholds = [30, 70]
3238

33-
# Geometry: gauge spans from 180° (left) to 0° (right)
39+
# Gauge geometry: 180° arc (left=0, right=100)
3440
angle_range = 180
3541
value_normalized = (value - min_value) / (max_value - min_value)
3642
needle_angle = 180 - value_normalized * angle_range
3743

38-
# Plot
39-
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
44+
# Plot — square canvas (2400×2400) for optimal gauge proportions
45+
fig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)
4046
ax.set_facecolor(PAGE_BG)
4147

42-
# Background zone wedges
48+
# Color zones with labels
4349
zone_colors = [ZONE_BAD, ZONE_WARN, ZONE_GOOD]
4450
zone_boundaries = [min_value] + thresholds + [max_value]
51+
zone_labels = ["Poor", "Fair", "Good"]
4552

4653
for i in range(len(zone_colors)):
4754
start_norm = (zone_boundaries[i] - min_value) / (max_value - min_value)
@@ -60,22 +67,36 @@
6067
)
6168
ax.add_patch(wedge)
6269

63-
# Inner cutout to clean the dial center (matches page background)
64-
inner_circle = mpatches.Wedge(center=(0, 0), r=0.65, theta1=0, theta2=180, facecolor=PAGE_BG, edgecolor="none")
65-
ax.add_patch(inner_circle)
70+
# Zone label at arc midpoint (tangential orientation)
71+
mid_norm = (start_norm + end_norm) / 2
72+
mid_angle = 180 - mid_norm * angle_range
73+
mid_rad = np.radians(mid_angle)
74+
ax.text(
75+
0.85 * np.cos(mid_rad),
76+
0.85 * np.sin(mid_rad),
77+
zone_labels[i],
78+
ha="center",
79+
va="center",
80+
fontsize=8,
81+
fontweight="bold",
82+
color=PAGE_BG,
83+
rotation=mid_angle - 90,
84+
)
85+
86+
# Inner fill (matches page background, cleans dial center)
87+
ax.add_patch(mpatches.Wedge(center=(0, 0), r=0.65, theta1=0, theta2=180, facecolor=PAGE_BG, edgecolor="none"))
6688

67-
# Tick marks: major (with labels) and minor (between)
89+
# Tick marks: minor and major
6890
major_ticks = [0, 25, 50, 75, 100]
6991
minor_ticks = [t for t in range(0, 101, 5) if t not in major_ticks]
7092

7193
for tick in minor_ticks:
7294
tick_norm = (tick - min_value) / (max_value - min_value)
7395
tick_angle = 180 - tick_norm * angle_range
7496
tick_rad = np.radians(tick_angle)
75-
inner_r, outer_r = 1.02, 1.05
7697
ax.plot(
77-
[inner_r * np.cos(tick_rad), outer_r * np.cos(tick_rad)],
78-
[inner_r * np.sin(tick_rad), outer_r * np.sin(tick_rad)],
98+
[1.02 * np.cos(tick_rad), 1.05 * np.cos(tick_rad)],
99+
[1.02 * np.sin(tick_rad), 1.05 * np.sin(tick_rad)],
79100
color=INK_SOFT,
80101
linewidth=1.5,
81102
)
@@ -84,48 +105,51 @@
84105
tick_norm = (tick - min_value) / (max_value - min_value)
85106
tick_angle = 180 - tick_norm * angle_range
86107
tick_rad = np.radians(tick_angle)
87-
inner_r, outer_r = 1.02, 1.09
88108
ax.plot(
89-
[inner_r * np.cos(tick_rad), outer_r * np.cos(tick_rad)],
90-
[inner_r * np.sin(tick_rad), outer_r * np.sin(tick_rad)],
109+
[1.02 * np.cos(tick_rad), 1.09 * np.cos(tick_rad)],
110+
[1.02 * np.sin(tick_rad), 1.09 * np.sin(tick_rad)],
91111
color=INK,
92112
linewidth=3,
93113
)
94-
label_r = 1.19
95114
ax.text(
96-
label_r * np.cos(tick_rad),
97-
label_r * np.sin(tick_rad),
115+
1.19 * np.cos(tick_rad),
116+
1.19 * np.sin(tick_rad),
98117
str(tick),
99118
ha="center",
100119
va="center",
101-
fontsize=18,
120+
fontsize=16,
102121
fontweight="bold",
103122
color=INK_SOFT,
104123
)
105124

106125
# Needle
107126
needle_rad = np.radians(needle_angle)
108-
needle_length = 0.78
109-
needle_x = needle_length * np.cos(needle_rad)
110-
needle_y = needle_length * np.sin(needle_rad)
111-
ax.plot([0, needle_x], [0, needle_y], color=INK, linewidth=6, solid_capstyle="round", zorder=9)
127+
ax.plot(
128+
[0, 0.78 * np.cos(needle_rad)],
129+
[0, 0.78 * np.sin(needle_rad)],
130+
color=INK,
131+
linewidth=5,
132+
solid_capstyle="round",
133+
zorder=9,
134+
)
112135

113136
# Center cap (two-tone for definition)
114137
ax.add_patch(plt.Circle((0, 0), 0.09, facecolor=INK, edgecolor="none", zorder=10))
115138
ax.add_patch(plt.Circle((0, 0), 0.035, facecolor=PAGE_BG, edgecolor="none", zorder=11))
116139

117140
# Value label and context
118-
ax.text(0, -0.25, f"{value}", ha="center", va="center", fontsize=56, fontweight="bold", color=ZONE_GOOD)
119-
ax.text(0, -0.47, "Current Sales", ha="center", va="center", fontsize=20, color=INK_MUTED)
141+
ax.text(0, -0.22, f"{value}", ha="center", va="center", fontsize=56, fontweight="bold", color=ZONE_GOOD)
142+
ax.text(0, -0.46, "Current Sales", ha="center", va="center", fontsize=18, color=INK_MUTED)
120143

121144
# Title
122-
ax.set_title("gauge-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK, pad=20)
145+
title = "gauge-basic · python · matplotlib · anyplot.ai"
146+
ax.set_title(title, fontsize=12, fontweight="medium", color=INK, pad=20)
123147

124-
# Frame
148+
# Frame — square axes to match square canvas
125149
ax.set_aspect("equal")
126150
ax.set_xlim(-1.5, 1.5)
127-
ax.set_ylim(-0.7, 1.5)
151+
ax.set_ylim(-1.15, 1.85)
128152
ax.axis("off")
129153

130-
plt.tight_layout()
131-
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)
154+
fig.subplots_adjust(left=0.03, right=0.97, top=0.88, bottom=0.05)
155+
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

0 commit comments

Comments
 (0)