|
1 | 1 | """ anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | 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) |
8 | 15 |
|
9 | 16 | import matplotlib.patches as mpatches |
10 | 17 | import matplotlib.pyplot as plt |
|
14 | 21 | # Theme tokens |
15 | 22 | THEME = os.getenv("ANYPLOT_THEME", "light") |
16 | 23 | PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
17 | | -ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
18 | 24 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
19 | 25 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
20 | 26 | INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
21 | 27 |
|
22 | | -# imprint semantic anchors (red / amber / green traffic-light) |
| 28 | +# Imprint semantic anchors (red / amber / green traffic-light) |
23 | 29 | ZONE_BAD = "#AE3030" # matte red — semantic bad |
24 | 30 | ZONE_WARN = "#DDCC77" # amber — semantic warning |
25 | 31 | ZONE_GOOD = "#009E73" # brand green — semantic good |
26 | 32 |
|
27 | 33 | # Data |
28 | | -value = 72 # Current sales value |
| 34 | +value = 72 |
29 | 35 | min_value = 0 |
30 | 36 | max_value = 100 |
31 | | -thresholds = [30, 70] # Boundaries for bad/warn/good zones |
| 37 | +thresholds = [30, 70] |
32 | 38 |
|
33 | | -# Geometry: gauge spans from 180° (left) to 0° (right) |
| 39 | +# Gauge geometry: 180° arc (left=0, right=100) |
34 | 40 | angle_range = 180 |
35 | 41 | value_normalized = (value - min_value) / (max_value - min_value) |
36 | 42 | needle_angle = 180 - value_normalized * angle_range |
37 | 43 |
|
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) |
40 | 46 | ax.set_facecolor(PAGE_BG) |
41 | 47 |
|
42 | | -# Background zone wedges |
| 48 | +# Color zones with labels |
43 | 49 | zone_colors = [ZONE_BAD, ZONE_WARN, ZONE_GOOD] |
44 | 50 | zone_boundaries = [min_value] + thresholds + [max_value] |
| 51 | +zone_labels = ["Poor", "Fair", "Good"] |
45 | 52 |
|
46 | 53 | for i in range(len(zone_colors)): |
47 | 54 | start_norm = (zone_boundaries[i] - min_value) / (max_value - min_value) |
|
60 | 67 | ) |
61 | 68 | ax.add_patch(wedge) |
62 | 69 |
|
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")) |
66 | 88 |
|
67 | | -# Tick marks: major (with labels) and minor (between) |
| 89 | +# Tick marks: minor and major |
68 | 90 | major_ticks = [0, 25, 50, 75, 100] |
69 | 91 | minor_ticks = [t for t in range(0, 101, 5) if t not in major_ticks] |
70 | 92 |
|
71 | 93 | for tick in minor_ticks: |
72 | 94 | tick_norm = (tick - min_value) / (max_value - min_value) |
73 | 95 | tick_angle = 180 - tick_norm * angle_range |
74 | 96 | tick_rad = np.radians(tick_angle) |
75 | | - inner_r, outer_r = 1.02, 1.05 |
76 | 97 | 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)], |
79 | 100 | color=INK_SOFT, |
80 | 101 | linewidth=1.5, |
81 | 102 | ) |
|
84 | 105 | tick_norm = (tick - min_value) / (max_value - min_value) |
85 | 106 | tick_angle = 180 - tick_norm * angle_range |
86 | 107 | tick_rad = np.radians(tick_angle) |
87 | | - inner_r, outer_r = 1.02, 1.09 |
88 | 108 | 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)], |
91 | 111 | color=INK, |
92 | 112 | linewidth=3, |
93 | 113 | ) |
94 | | - label_r = 1.19 |
95 | 114 | 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), |
98 | 117 | str(tick), |
99 | 118 | ha="center", |
100 | 119 | va="center", |
101 | | - fontsize=18, |
| 120 | + fontsize=16, |
102 | 121 | fontweight="bold", |
103 | 122 | color=INK_SOFT, |
104 | 123 | ) |
105 | 124 |
|
106 | 125 | # Needle |
107 | 126 | 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 | +) |
112 | 135 |
|
113 | 136 | # Center cap (two-tone for definition) |
114 | 137 | ax.add_patch(plt.Circle((0, 0), 0.09, facecolor=INK, edgecolor="none", zorder=10)) |
115 | 138 | ax.add_patch(plt.Circle((0, 0), 0.035, facecolor=PAGE_BG, edgecolor="none", zorder=11)) |
116 | 139 |
|
117 | 140 | # 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) |
120 | 143 |
|
121 | 144 | # 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) |
123 | 147 |
|
124 | | -# Frame |
| 148 | +# Frame — square axes to match square canvas |
125 | 149 | ax.set_aspect("equal") |
126 | 150 | ax.set_xlim(-1.5, 1.5) |
127 | | -ax.set_ylim(-0.7, 1.5) |
| 151 | +ax.set_ylim(-1.15, 1.85) |
128 | 152 | ax.axis("off") |
129 | 153 |
|
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