|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | gauge-basic: Basic Gauge Chart |
3 | | -Library: matplotlib 3.10.8 | Python 3.13.11 |
4 | | -Quality: 95/100 | Created: 2025-12-23 |
| 3 | +Library: matplotlib 3.10.9 | Python 3.14.4 |
| 4 | +Quality: 91/100 | Updated: 2026-04-25 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import matplotlib.patches as mpatches |
8 | 10 | import matplotlib.pyplot as plt |
9 | 11 | import numpy as np |
10 | 12 |
|
11 | 13 |
|
| 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 | + |
12 | 27 | # Data |
13 | 28 | value = 72 # Current sales value |
14 | 29 | min_value = 0 |
15 | 30 | max_value = 100 |
16 | | -thresholds = [30, 70] # Boundaries for red/yellow/green zones |
| 31 | +thresholds = [30, 70] # Boundaries for bad/warn/good zones |
17 | 32 |
|
18 | | -# Calculate angles (gauge spans from 180° to 0°, i.e., left to right) |
19 | | -angle_range = 180 # Semi-circular gauge |
| 33 | +# Geometry: gauge spans from 180° (left) to 0° (right) |
| 34 | +angle_range = 180 |
20 | 35 | value_normalized = (value - min_value) / (max_value - min_value) |
21 | | -needle_angle = 180 - value_normalized * angle_range # Convert to degrees (180=left, 0=right) |
| 36 | +needle_angle = 180 - value_normalized * angle_range |
22 | 37 |
|
23 | | -# Create plot (4800x2700 px) |
24 | | -fig, ax = plt.subplots(figsize=(16, 9)) |
| 38 | +# Plot |
| 39 | +fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG) |
| 40 | +ax.set_facecolor(PAGE_BG) |
25 | 41 |
|
26 | | -# Draw gauge background zones (wedges) |
27 | | -zone_colors = ["#E74C3C", "#F1C40F", "#2ECC71"] # Red, Yellow, Green |
| 42 | +# Background zone wedges |
| 43 | +zone_colors = [ZONE_BAD, ZONE_WARN, ZONE_GOOD] |
28 | 44 | zone_boundaries = [min_value] + thresholds + [max_value] |
29 | 45 |
|
30 | 46 | for i in range(len(zone_colors)): |
31 | 47 | start_norm = (zone_boundaries[i] - min_value) / (max_value - min_value) |
32 | 48 | end_norm = (zone_boundaries[i + 1] - min_value) / (max_value - min_value) |
33 | | - # Convert to angles (180° to 0°) |
34 | 49 | theta1 = 180 - end_norm * angle_range |
35 | 50 | theta2 = 180 - start_norm * angle_range |
36 | 51 | wedge = mpatches.Wedge( |
|
40 | 55 | theta2=theta2, |
41 | 56 | width=0.3, |
42 | 57 | facecolor=zone_colors[i], |
43 | | - edgecolor="white", |
| 58 | + edgecolor=PAGE_BG, |
44 | 59 | linewidth=2, |
45 | 60 | ) |
46 | 61 | ax.add_patch(wedge) |
47 | 62 |
|
48 | | -# Draw inner arc (white background for cleaner look) |
49 | | -inner_circle = mpatches.Wedge(center=(0, 0), r=0.65, theta1=0, theta2=180, facecolor="white", edgecolor="none") |
| 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") |
50 | 65 | ax.add_patch(inner_circle) |
51 | 66 |
|
52 | | -# Draw the needle |
53 | | -needle_rad = np.radians(needle_angle) |
54 | | -needle_length = 0.75 |
55 | | -needle_x = needle_length * np.cos(needle_rad) |
56 | | -needle_y = needle_length * np.sin(needle_rad) |
57 | | - |
58 | | -# Needle line |
59 | | -ax.plot([0, needle_x], [0, needle_y], color="#2C3E50", linewidth=6, solid_capstyle="round") |
60 | | - |
61 | | -# Needle center cap |
62 | | -center_circle = plt.Circle((0, 0), 0.08, color="#2C3E50", zorder=10) |
63 | | -ax.add_patch(center_circle) |
| 67 | +# Tick marks: major (with labels) and minor (between) |
| 68 | +major_ticks = [0, 25, 50, 75, 100] |
| 69 | +minor_ticks = [t for t in range(0, 101, 5) if t not in major_ticks] |
64 | 70 |
|
65 | | -# Add tick marks and labels around the gauge |
66 | | -tick_values = [0, 25, 50, 75, 100] |
67 | | -for tick in tick_values: |
| 71 | +for tick in minor_ticks: |
68 | 72 | tick_norm = (tick - min_value) / (max_value - min_value) |
69 | 73 | tick_angle = 180 - tick_norm * angle_range |
70 | 74 | tick_rad = np.radians(tick_angle) |
| 75 | + inner_r, outer_r = 1.02, 1.05 |
| 76 | + 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)], |
| 79 | + color=INK_SOFT, |
| 80 | + linewidth=1.5, |
| 81 | + ) |
71 | 82 |
|
72 | | - # Tick mark |
73 | | - inner_r = 1.02 |
74 | | - outer_r = 1.08 |
| 83 | +for tick in major_ticks: |
| 84 | + tick_norm = (tick - min_value) / (max_value - min_value) |
| 85 | + tick_angle = 180 - tick_norm * angle_range |
| 86 | + tick_rad = np.radians(tick_angle) |
| 87 | + inner_r, outer_r = 1.02, 1.09 |
75 | 88 | ax.plot( |
76 | 89 | [inner_r * np.cos(tick_rad), outer_r * np.cos(tick_rad)], |
77 | 90 | [inner_r * np.sin(tick_rad), outer_r * np.sin(tick_rad)], |
78 | | - color="#333333", |
| 91 | + color=INK, |
79 | 92 | linewidth=3, |
80 | 93 | ) |
81 | | - |
82 | | - # Tick label |
83 | | - label_r = 1.18 |
| 94 | + label_r = 1.19 |
84 | 95 | ax.text( |
85 | 96 | label_r * np.cos(tick_rad), |
86 | 97 | label_r * np.sin(tick_rad), |
|
89 | 100 | va="center", |
90 | 101 | fontsize=18, |
91 | 102 | fontweight="bold", |
92 | | - color="#333333", |
| 103 | + color=INK_SOFT, |
93 | 104 | ) |
94 | 105 |
|
95 | | -# Display the current value prominently below the gauge |
96 | | -ax.text(0, -0.25, f"{value}", ha="center", va="center", fontsize=48, fontweight="bold", color="#306998") |
| 106 | +# Needle |
| 107 | +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) |
| 112 | + |
| 113 | +# Center cap (two-tone for definition) |
| 114 | +ax.add_patch(plt.Circle((0, 0), 0.09, facecolor=INK, edgecolor="none", zorder=10)) |
| 115 | +ax.add_patch(plt.Circle((0, 0), 0.035, facecolor=PAGE_BG, edgecolor="none", zorder=11)) |
97 | 116 |
|
98 | | -# Add "Current Sales" label below value |
99 | | -ax.text(0, -0.45, "Current Sales", ha="center", va="center", fontsize=20, color="#666666") |
| 117 | +# 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) |
100 | 120 |
|
101 | | -# Add title |
102 | | -ax.set_title("gauge-basic · matplotlib · pyplots.ai", fontsize=24, pad=20) |
| 121 | +# Title |
| 122 | +ax.set_title("gauge-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK, pad=20) |
103 | 123 |
|
104 | | -# Set equal aspect ratio and limits |
| 124 | +# Frame |
105 | 125 | ax.set_aspect("equal") |
106 | 126 | ax.set_xlim(-1.5, 1.5) |
107 | 127 | ax.set_ylim(-0.7, 1.5) |
108 | 128 | ax.axis("off") |
109 | 129 |
|
110 | 130 | plt.tight_layout() |
111 | | -plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
| 131 | +plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) |
0 commit comments