|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | rose-basic: Basic Rose Chart |
3 | | -Library: plotnine 0.15.2 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: plotnine 0.15.3 | Python 3.13.13 |
| 4 | +Quality: 85/100 | Updated: 2026-04-30 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import math |
| 8 | +import os |
| 9 | +import sys |
| 10 | + |
| 11 | + |
| 12 | +# Remove this file's own directory from sys.path so that "plotnine" resolves |
| 13 | +# to the installed library rather than this file (Python 3.13 naming-collision fix). |
| 14 | +_script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 15 | +sys.path = [p for p in sys.path if os.path.realpath(p) != os.path.realpath(_script_dir)] |
8 | 16 |
|
9 | 17 | import numpy as np |
10 | 18 | import pandas as pd |
| 19 | +from matplotlib import colormaps |
| 20 | +from matplotlib.colors import Normalize, to_hex |
11 | 21 | from plotnine import ( |
12 | 22 | aes, |
13 | 23 | element_blank, |
| 24 | + element_rect, |
14 | 25 | element_text, |
15 | 26 | geom_line, |
16 | 27 | geom_polygon, |
|
24 | 35 | ) |
25 | 36 |
|
26 | 37 |
|
| 38 | +# Theme tokens |
| 39 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 40 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 41 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 42 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 43 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
| 44 | + |
27 | 45 | # Data - Monthly rainfall (mm) for a temperate climate |
28 | 46 | months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] |
29 | 47 | rainfall = [78, 62, 55, 48, 52, 68, 82, 85, 72, 88, 95, 82] |
30 | 48 |
|
31 | 49 | n = len(months) |
32 | 50 |
|
| 51 | +# Viridis colors per month based on rainfall value (continuous colormap for value-encoded data) |
| 52 | +_norm = Normalize(vmin=min(rainfall), vmax=max(rainfall)) |
| 53 | +_cmap = colormaps["viridis"] |
| 54 | +month_colors = {month: to_hex(_cmap(_norm(val))) for month, val in zip(months, rainfall, strict=True)} |
| 55 | + |
33 | 56 | # Create wedge polygons for each month |
34 | | -# Each wedge is a triangle from center to the arc |
| 57 | +n_arc_points = 30 |
35 | 58 | wedge_rows = [] |
36 | | -n_arc_points = 30 # Points along the arc for smooth edges |
37 | | - |
38 | 59 | for i, (month, value) in enumerate(zip(months, rainfall, strict=True)): |
39 | | - # Starting angle (12 o'clock = pi/2, going clockwise) |
40 | | - # Adjust so January is at top (index 0 starts at pi/2) |
41 | | - # Use negative direction for clockwise motion |
42 | 60 | start_angle = math.pi / 2 - (i * 2 * math.pi / n) |
43 | 61 | end_angle = math.pi / 2 - ((i + 1) * 2 * math.pi / n) |
44 | | - |
45 | | - # Small gap between wedges |
46 | 62 | gap = 0.02 |
47 | 63 | start_angle += gap |
48 | 64 | end_angle -= gap |
49 | | - |
50 | | - # Center point |
51 | 65 | wedge_rows.append({"x": 0, "y": 0, "month": month, "order": 0}) |
52 | | - |
53 | | - # Arc points |
54 | | - arc_angles = np.linspace(start_angle, end_angle, n_arc_points) |
55 | | - for j, angle in enumerate(arc_angles): |
56 | | - x = value * math.cos(angle) |
57 | | - y = value * math.sin(angle) |
58 | | - wedge_rows.append({"x": x, "y": y, "month": month, "order": j + 1}) |
59 | | - |
60 | | - # Close back to center |
| 66 | + for j, angle in enumerate(np.linspace(start_angle, end_angle, n_arc_points)): |
| 67 | + wedge_rows.append({"x": value * math.cos(angle), "y": value * math.sin(angle), "month": month, "order": j + 1}) |
61 | 68 | wedge_rows.append({"x": 0, "y": 0, "month": month, "order": n_arc_points + 1}) |
62 | 69 |
|
63 | 70 | df = pd.DataFrame(wedge_rows) |
64 | | - |
65 | | -# Preserve month order |
66 | 71 | df["month"] = pd.Categorical(df["month"], categories=months, ordered=True) |
67 | 72 |
|
68 | | -# Colors - gradient based on rainfall values |
69 | | -# Using Python Blue (#306998) as base with varying intensity |
70 | | -base_blue = np.array([48, 105, 152]) # RGB for #306998 |
71 | | -python_yellow = np.array([255, 212, 59]) # RGB for #FFD43B |
72 | | - |
73 | | -# Normalize rainfall for color mapping |
74 | | -min_val, max_val = min(rainfall), max(rainfall) |
75 | | -colors = [] |
76 | | -for value in rainfall: |
77 | | - t = (value - min_val) / (max_val - min_val) # 0 to 1 |
78 | | - # Interpolate from blue to yellow |
79 | | - rgb = (1 - t) * base_blue + t * python_yellow |
80 | | - colors.append(f"#{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}") |
81 | | - |
82 | | -color_dict = dict(zip(months, colors, strict=True)) |
83 | | - |
84 | | -# Create radial gridlines (circles at 20, 40, 60, 80, 100) |
| 73 | +# Radial gridlines (circles) |
85 | 74 | grid_rows = [] |
86 | 75 | grid_angles = np.linspace(0, 2 * math.pi, 101) |
87 | 76 | for radius in [20, 40, 60, 80, 100]: |
88 | 77 | for angle in grid_angles: |
89 | 78 | grid_rows.append({"x": radius * math.cos(angle), "y": radius * math.sin(angle), "radius": radius}) |
90 | | - |
91 | 79 | grid_df = pd.DataFrame(grid_rows) |
92 | 80 |
|
93 | | -# Create spoke lines (one for each month boundary) |
| 81 | +# Spoke lines from center to edge |
94 | 82 | spoke_rows = [] |
95 | 83 | for i in range(n): |
96 | 84 | angle = math.pi / 2 - (i * 2 * math.pi / n) |
97 | 85 | spoke_rows.append({"x": 0, "y": 0, "spoke_id": i}) |
98 | 86 | spoke_rows.append({"x": 105 * math.cos(angle), "y": 105 * math.sin(angle), "spoke_id": i}) |
99 | | - |
100 | 87 | spoke_df = pd.DataFrame(spoke_rows) |
101 | 88 |
|
102 | | -# Create month labels positioned outside the chart |
| 89 | +# Month labels positioned outside the chart |
103 | 90 | label_rows = [] |
104 | 91 | for i, month in enumerate(months): |
105 | | - # Center angle of each month's wedge |
106 | 92 | center_angle = math.pi / 2 - ((i + 0.5) * 2 * math.pi / n) |
107 | | - label_rows.append({"label": month, "x": 115 * math.cos(center_angle), "y": 115 * math.sin(center_angle)}) |
108 | | - |
| 93 | + label_rows.append({"label": month, "x": 120 * math.cos(center_angle), "y": 120 * math.sin(center_angle)}) |
109 | 94 | label_df = pd.DataFrame(label_rows) |
110 | 95 |
|
111 | | -# Create value labels on gridlines |
112 | | -value_label_rows = [] |
113 | | -for radius in [20, 40, 60, 80, 100]: |
114 | | - value_label_rows.append({"label": str(radius), "x": 5, "y": radius + 3}) |
115 | | - |
| 96 | +# Value labels positioned along the top-right spoke |
| 97 | +value_label_rows = [{"label": str(r), "x": 6, "y": r + 4} for r in [20, 40, 60, 80, 100]] |
116 | 98 | value_label_df = pd.DataFrame(value_label_rows) |
117 | 99 |
|
118 | 100 | # Plot |
119 | 101 | plot = ( |
120 | 102 | ggplot() |
121 | | - # Gridlines (circles) |
122 | 103 | + geom_line( |
123 | | - aes(x="x", y="y", group="radius"), data=grid_df, color="#CCCCCC", size=0.5, alpha=0.6, linetype="dashed" |
| 104 | + aes(x="x", y="y", group="radius"), data=grid_df, color=INK_SOFT, size=0.4, alpha=0.15, linetype="dashed" |
124 | 105 | ) |
125 | | - # Spoke lines |
126 | | - + geom_line(aes(x="x", y="y", group="spoke_id"), data=spoke_df, color="#DDDDDD", size=0.3, alpha=0.5) |
127 | | - # Rose wedges |
128 | | - + geom_polygon(aes(x="x", y="y", fill="month", group="month"), data=df, color="#2C3E50", size=0.3, alpha=0.85) |
129 | | - # Month labels |
130 | | - + geom_text(aes(x="x", y="y", label="label"), data=label_df, size=14, fontweight="bold", color="#333333") |
131 | | - # Value labels |
132 | | - + geom_text(aes(x="x", y="y", label="label"), data=value_label_df, size=10, color="#666666") |
133 | | - # Colors |
134 | | - + scale_fill_manual(values=color_dict) |
135 | | - # Axis scaling |
136 | | - + scale_x_continuous(limits=(-135, 135)) |
137 | | - + scale_y_continuous(limits=(-135, 135)) |
138 | | - # Labels and title |
139 | | - + labs(title="Monthly Rainfall (mm) · rose-basic · plotnine · pyplots.ai") |
140 | | - # Theme for clean rose chart appearance |
| 106 | + + geom_line(aes(x="x", y="y", group="spoke_id"), data=spoke_df, color=INK_SOFT, size=0.3, alpha=0.12) |
| 107 | + + geom_polygon(aes(x="x", y="y", fill="month", group="month"), data=df, color=PAGE_BG, size=0.3, alpha=0.88) |
| 108 | + + geom_text(aes(x="x", y="y", label="label"), data=label_df, size=14, fontweight="bold", color=INK) |
| 109 | + + geom_text(aes(x="x", y="y", label="label"), data=value_label_df, size=10, color=INK_MUTED) |
| 110 | + + scale_fill_manual(values=month_colors) |
| 111 | + + scale_x_continuous(limits=(-142, 142)) |
| 112 | + + scale_y_continuous(limits=(-142, 142)) |
| 113 | + + labs(title="Monthly Rainfall (mm) · rose-basic · plotnine · anyplot.ai") |
141 | 114 | + theme( |
142 | 115 | figure_size=(12, 12), |
143 | | - plot_title=element_text(size=22, ha="center"), |
| 116 | + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
| 117 | + panel_background=element_rect(fill=PAGE_BG), |
| 118 | + plot_title=element_text(size=22, ha="center", color=INK), |
144 | 119 | axis_title=element_blank(), |
145 | 120 | axis_text=element_blank(), |
146 | 121 | axis_ticks=element_blank(), |
147 | 122 | axis_line=element_blank(), |
148 | 123 | panel_grid_major=element_blank(), |
149 | 124 | panel_grid_minor=element_blank(), |
150 | | - panel_background=element_blank(), |
| 125 | + panel_border=element_blank(), |
151 | 126 | legend_position="none", |
152 | 127 | ) |
153 | 128 | ) |
154 | 129 |
|
155 | 130 | # Save |
156 | | -plot.save("plot.png", dpi=300) |
| 131 | +plot.save(f"plot-{THEME}.png", dpi=300) |
0 commit comments