|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | gauge-basic: Basic Gauge Chart |
3 | | -Library: seaborn 0.13.2 | Python 3.13.11 |
4 | | -Quality: 90/100 | Created: 2025-12-23 |
| 3 | +Library: seaborn 0.13.2 | Python 3.14.4 |
| 4 | +Quality: 89/100 | Updated: 2026-04-25 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
| 9 | +import matplotlib.patheffects as pe |
7 | 10 | import matplotlib.pyplot as plt |
8 | 11 | import numpy as np |
9 | | -import pandas as pd |
10 | 12 | import seaborn as sns |
| 13 | +from matplotlib.patches import Circle, Wedge |
| 14 | + |
| 15 | + |
| 16 | +# Theme tokens |
| 17 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 18 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 19 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 20 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
11 | 21 |
|
| 22 | +# Okabe-Ito zone palette (semantic gauge mapping: vermillion / orange / brand green) |
| 23 | +ZONE_LOW, ZONE_MED, ZONE_HIGH = sns.color_palette(["#D55E00", "#E69F00", "#009E73"]) |
12 | 24 |
|
13 | | -# Data - Sales performance gauge |
14 | | -value = 72 # Current sales performance |
| 25 | +# Data — sales performance gauge |
| 26 | +value = 72 |
15 | 27 | min_value = 0 |
16 | 28 | max_value = 100 |
17 | | -thresholds = [30, 70] # Zone boundaries |
| 29 | +thresholds = [30, 70] |
18 | 30 |
|
19 | | -# Create figure with larger gauge |
20 | | -fig, ax = plt.subplots(figsize=(16, 9)) |
21 | | -sns.set_theme(style="white") |
22 | | - |
23 | | -# Gauge parameters - larger for better canvas utilization |
24 | | -center = (0.5, 0.30) |
25 | | -radius = 0.42 |
26 | | -width = 0.18 |
| 31 | +# Gauge geometry |
| 32 | +center = (0.5, 0.45) |
| 33 | +radius = 0.40 |
| 34 | +width = 0.16 |
27 | 35 | start_angle = 180 |
28 | 36 | end_angle = 0 |
29 | 37 | angle_range = start_angle - end_angle |
30 | 38 | value_range = max_value - min_value |
31 | 39 |
|
32 | | -# Colorblind-safe palette using seaborn's colorblind palette |
33 | | -cb_palette = sns.color_palette("colorblind", 3) |
34 | | -zone_colors = [cb_palette[2], cb_palette[1], cb_palette[0]] # Blue-ish, Orange, Green-ish order for low-med-high |
35 | | - |
36 | | -# Draw gauge arc segments using seaborn scatterplot for the zone markers |
37 | | -# Create data for zone indicator points along the arc |
38 | | -n_points_per_zone = 50 |
39 | 40 | zone_boundaries = [min_value] + thresholds + [max_value] |
40 | | -zone_data = [] |
| 41 | +zone_names = ["Low", "Medium", "High"] |
| 42 | +zone_colors = [ZONE_LOW, ZONE_MED, ZONE_HIGH] |
| 43 | + |
| 44 | +# Plot |
| 45 | +sns.set_theme( |
| 46 | + style="white", |
| 47 | + rc={ |
| 48 | + "figure.facecolor": PAGE_BG, |
| 49 | + "axes.facecolor": PAGE_BG, |
| 50 | + "text.color": INK, |
| 51 | + "axes.labelcolor": INK, |
| 52 | + "xtick.color": INK_SOFT, |
| 53 | + "ytick.color": INK_SOFT, |
| 54 | + }, |
| 55 | +) |
| 56 | +fig, ax = plt.subplots(figsize=(12, 12), facecolor=PAGE_BG) |
| 57 | +ax.set_facecolor(PAGE_BG) |
41 | 58 |
|
| 59 | +# Arc — three coloured zones drawn as smooth Wedge patches |
42 | 60 | for i in range(len(zone_boundaries) - 1): |
43 | | - zone_start = zone_boundaries[i] |
44 | | - zone_end = zone_boundaries[i + 1] |
45 | | - zone_name = ["Low", "Medium", "High"][i] |
46 | | - |
47 | | - for v in np.linspace(zone_start, zone_end, n_points_per_zone, endpoint=(i == len(zone_boundaries) - 2)): |
48 | | - angle = start_angle - (v - min_value) / value_range * angle_range |
49 | | - rad = np.radians(angle) |
50 | | - # Points along the middle of the arc width |
51 | | - for r_offset in np.linspace(-width / 2 + 0.01, width / 2 - 0.01, 8): |
52 | | - r = radius - width / 2 + r_offset + width / 2 |
53 | | - x = center[0] + r * np.cos(rad) |
54 | | - y = center[1] + r * np.sin(rad) |
55 | | - zone_data.append({"x": x, "y": y, "zone": zone_name, "value": v}) |
56 | | - |
57 | | -df_zones = pd.DataFrame(zone_data) |
58 | | - |
59 | | -# Use seaborn scatterplot to draw the gauge arc zones with denser points for smooth appearance |
60 | | -sns.scatterplot( |
61 | | - data=df_zones, |
62 | | - x="x", |
63 | | - y="y", |
64 | | - hue="zone", |
65 | | - hue_order=["Low", "Medium", "High"], |
66 | | - palette=[zone_colors[0], zone_colors[1], zone_colors[2]], |
67 | | - s=180, |
68 | | - marker="o", |
69 | | - edgecolor="none", |
70 | | - alpha=1.0, |
71 | | - legend=False, |
72 | | - ax=ax, |
73 | | -) |
| 61 | + z_start_angle = start_angle - (zone_boundaries[i] - min_value) / value_range * angle_range |
| 62 | + z_end_angle = start_angle - (zone_boundaries[i + 1] - min_value) / value_range * angle_range |
| 63 | + ax.add_patch( |
| 64 | + Wedge( |
| 65 | + center=center, |
| 66 | + r=radius + width / 2, |
| 67 | + theta1=z_end_angle, |
| 68 | + theta2=z_start_angle, |
| 69 | + width=width, |
| 70 | + facecolor=zone_colors[i], |
| 71 | + edgecolor="none", |
| 72 | + linewidth=0, |
| 73 | + zorder=2, |
| 74 | + ) |
| 75 | + ) |
74 | 76 |
|
75 | | -# Add thin white arc lines at zone boundaries using seaborn for visual separation |
| 77 | +# Boundary cuts between zones — short radial line, theme-visible color |
76 | 78 | for threshold in thresholds: |
77 | 79 | boundary_angle = start_angle - (threshold - min_value) / value_range * angle_range |
78 | 80 | rad = np.radians(boundary_angle) |
79 | | - boundary_line_data = [] |
80 | | - for r in np.linspace(radius - width, radius, 10): |
81 | | - boundary_line_data.append({"x": center[0] + r * np.cos(rad), "y": center[1] + r * np.sin(rad)}) |
82 | | - boundary_df = pd.DataFrame(boundary_line_data) |
83 | | - sns.lineplot(data=boundary_df, x="x", y="y", color="white", linewidth=3, ax=ax, legend=False) |
84 | | - |
85 | | -# Create needle indicator data point using seaborn |
| 81 | + rs = (radius - width / 2, radius + width / 2) |
| 82 | + ax.plot( |
| 83 | + [center[0] + r * np.cos(rad) for r in rs], |
| 84 | + [center[1] + r * np.sin(rad) for r in rs], |
| 85 | + color=INK_SOFT, |
| 86 | + linewidth=3, |
| 87 | + zorder=3, |
| 88 | + solid_capstyle="butt", |
| 89 | + ) |
| 90 | + |
| 91 | +# Needle |
86 | 92 | needle_angle = start_angle - (value - min_value) / value_range * angle_range |
87 | 93 | needle_rad = np.radians(needle_angle) |
88 | | -needle_length = radius - width / 2 |
89 | | - |
90 | | -# Draw needle line |
| 94 | +needle_length = radius + width / 2 - 0.015 |
91 | 95 | needle_tip_x = center[0] + needle_length * np.cos(needle_rad) |
92 | 96 | needle_tip_y = center[1] + needle_length * np.sin(needle_rad) |
| 97 | +ax.plot([center[0], needle_tip_x], [center[1], needle_tip_y], color=INK, linewidth=6, zorder=12, solid_capstyle="round") |
93 | 98 |
|
94 | | -# Use seaborn lineplot for the needle |
95 | | -needle_df = pd.DataFrame({"x": [center[0], needle_tip_x], "y": [center[1], needle_tip_y]}) |
96 | | -sns.lineplot(data=needle_df, x="x", y="y", color="#2C3E50", linewidth=6, ax=ax) |
97 | | - |
98 | | -# Draw needle tip marker using seaborn scatterplot - larger and more prominent |
99 | | -tip_df = pd.DataFrame({"x": [needle_tip_x], "y": [needle_tip_y]}) |
100 | | -# White outline for contrast |
101 | | -sns.scatterplot(data=tip_df, x="x", y="y", s=900, color="white", marker="v", ax=ax, zorder=9) |
102 | | -# Main tip marker |
103 | | -sns.scatterplot(data=tip_df, x="x", y="y", s=700, color="#E74C3C", marker="v", ax=ax, zorder=10) |
104 | | - |
105 | | -# Draw center hub using seaborn scatterplot |
106 | | -hub_df = pd.DataFrame({"x": [center[0]], "y": [center[1]]}) |
107 | | -sns.scatterplot(data=hub_df, x="x", y="y", s=800, color="#2C3E50", marker="o", ax=ax, zorder=11) |
| 99 | +# Hub — outer ring (page bg) + inner disc (ink) for clean contrast in both themes |
| 100 | +ax.add_patch(Circle(center, radius=0.045, facecolor=PAGE_BG, edgecolor="none", zorder=13)) |
| 101 | +ax.add_patch(Circle(center, radius=0.035, facecolor=INK, edgecolor="none", zorder=14)) |
108 | 102 |
|
109 | 103 | # Value display |
110 | | -ax.text( |
111 | | - center[0], center[1] - 0.22, f"{value}%", ha="center", va="center", fontsize=52, fontweight="bold", color="#2C3E50" |
112 | | -) |
113 | | - |
114 | | -# Min and max labels |
115 | | -ax.text( |
116 | | - center[0] - radius + width / 2, |
117 | | - center[1] - 0.10, |
118 | | - f"{min_value}", |
119 | | - ha="center", |
120 | | - va="top", |
121 | | - fontsize=22, |
122 | | - color="#555555", |
123 | | -) |
124 | | -ax.text( |
125 | | - center[0] + radius - width / 2, |
126 | | - center[1] - 0.10, |
127 | | - f"{max_value}", |
128 | | - ha="center", |
129 | | - va="top", |
130 | | - fontsize=22, |
131 | | - color="#555555", |
132 | | -) |
133 | | - |
134 | | -# Zone labels on the arc |
135 | | -zone_label_angles = [ |
136 | | - start_angle - (15 / value_range) * angle_range, |
137 | | - start_angle - (50 / value_range) * angle_range, |
138 | | - start_angle - (85 / value_range) * angle_range, |
| 104 | +ax.text(center[0], center[1] - 0.20, f"{value}%", ha="center", va="center", fontsize=56, fontweight="bold", color=INK) |
| 105 | + |
| 106 | +# Min / max endpoint labels |
| 107 | +for vlabel, x_off in [(min_value, -1), (max_value, 1)]: |
| 108 | + ax.text( |
| 109 | + center[0] + x_off * radius, center[1] - 0.06, f"{vlabel}", ha="center", va="top", fontsize=20, color=INK_SOFT |
| 110 | + ) |
| 111 | + |
| 112 | +# Zone labels on the arc — white text with dark stroke for legibility on every zone in both themes |
| 113 | +zone_label_centers = [ |
| 114 | + (thresholds[0] - min_value) / 2 + min_value, |
| 115 | + (thresholds[0] + thresholds[1]) / 2, |
| 116 | + (thresholds[1] + max_value) / 2, |
139 | 117 | ] |
140 | | -zone_label_names = ["Low", "Medium", "High"] |
141 | | -label_radius = radius - width / 2 |
142 | | - |
143 | | -for angle, label in zip(zone_label_angles, zone_label_names, strict=True): |
| 118 | +for v_center, label in zip(zone_label_centers, zone_names, strict=True): |
| 119 | + angle = start_angle - (v_center - min_value) / value_range * angle_range |
144 | 120 | rad = np.radians(angle) |
145 | | - x = center[0] + label_radius * np.cos(rad) |
146 | | - y = center[1] + label_radius * np.sin(rad) |
147 | | - # Text shadow/outline for better contrast on colored backgrounds |
148 | | - for dx, dy in [(-1, -1), (-1, 1), (1, -1), (1, 1), (-1, 0), (1, 0), (0, -1), (0, 1)]: |
149 | | - ax.text( |
150 | | - x + dx * 0.003, |
151 | | - y + dy * 0.003, |
152 | | - label, |
153 | | - ha="center", |
154 | | - va="center", |
155 | | - fontsize=18, |
156 | | - color="#1a1a1a", |
157 | | - fontweight="bold", |
158 | | - ) |
159 | | - ax.text(x, y, label, ha="center", va="center", fontsize=18, color="white", fontweight="bold") |
| 121 | + lx = center[0] + radius * np.cos(rad) |
| 122 | + ly = center[1] + radius * np.sin(rad) |
| 123 | + txt = ax.text(lx, ly, label, ha="center", va="center", fontsize=22, color="white", fontweight="bold", zorder=5) |
| 124 | + txt.set_path_effects([pe.withStroke(linewidth=3, foreground="#1A1A17")]) |
160 | 125 |
|
161 | | -# Title and subtitle |
162 | | -ax.set_title("gauge-basic · seaborn · pyplots.ai", fontsize=28, fontweight="bold", pad=20, color="#333333") |
163 | | -ax.text(center[0], 0.95, "Sales Performance", ha="center", va="top", fontsize=24, color="#555555") |
| 126 | +# Title |
| 127 | +ax.set_title("gauge-basic · seaborn · anyplot.ai", fontsize=24, fontweight="medium", pad=20, color=INK) |
164 | 128 |
|
165 | | -# Axis settings |
| 129 | +# Axis settings — purely a canvas |
166 | 130 | ax.set_xlim(0, 1) |
167 | 131 | ax.set_ylim(0, 1) |
168 | 132 | ax.set_aspect("equal") |
169 | 133 | ax.axis("off") |
170 | 134 |
|
171 | 135 | plt.tight_layout() |
172 | | -plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white") |
| 136 | +plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) |
0 commit comments