|
| 1 | +""" pyplots.ai |
| 2 | +polar-bar: Polar Bar Chart (Wind Rose) |
| 3 | +Library: plotnine 0.15.2 | Python 3.13.11 |
| 4 | +Quality: 91/100 | Created: 2025-12-30 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +from plotnine import ( |
| 10 | + aes, |
| 11 | + coord_fixed, |
| 12 | + element_blank, |
| 13 | + element_text, |
| 14 | + geom_path, |
| 15 | + geom_polygon, |
| 16 | + geom_segment, |
| 17 | + geom_text, |
| 18 | + ggplot, |
| 19 | + labs, |
| 20 | + scale_fill_manual, |
| 21 | + scale_x_continuous, |
| 22 | + scale_y_continuous, |
| 23 | + theme, |
| 24 | +) |
| 25 | + |
| 26 | + |
| 27 | +# Data - Wind direction frequencies (8 compass directions) |
| 28 | +np.random.seed(42) |
| 29 | +directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"] |
| 30 | +direction_angles = [0, 45, 90, 135, 180, 225, 270, 315] # Degrees from North, clockwise |
| 31 | +frequencies = [15, 8, 12, 5, 18, 22, 10, 7] |
| 32 | + |
| 33 | +# Create polygons for each bar (wedge shape) |
| 34 | +# Each bar spans ±22.5 degrees (45 degrees total width for 8 directions) |
| 35 | +bar_half_width = 18 # degrees, slightly less than 22.5 for visual gap |
| 36 | +bar_rows = [] |
| 37 | +bar_id = 0 |
| 38 | + |
| 39 | +for direction, angle, freq in zip(directions, direction_angles, frequencies, strict=True): |
| 40 | + # Create wedge polygon points (from center outward) |
| 41 | + # Start angle and end angle |
| 42 | + start_angle = angle - bar_half_width |
| 43 | + end_angle = angle + bar_half_width |
| 44 | + |
| 45 | + # Create polygon vertices: center -> arc at radius -> back to center |
| 46 | + # Convert to math convention: 0° at East, CCW positive |
| 47 | + # Polar convention: 0° at North (top), CW positive |
| 48 | + |
| 49 | + # Center point |
| 50 | + points = [(0, 0)] |
| 51 | + |
| 52 | + # Arc points at the outer radius |
| 53 | + arc_angles = np.linspace(start_angle, end_angle, 10) |
| 54 | + for a in arc_angles: |
| 55 | + # Convert from compass (N=0, CW) to math (E=0, CCW) |
| 56 | + theta = np.radians(90 - a) |
| 57 | + x = freq * np.cos(theta) |
| 58 | + y = freq * np.sin(theta) |
| 59 | + points.append((x, y)) |
| 60 | + |
| 61 | + # Close back to center |
| 62 | + points.append((0, 0)) |
| 63 | + |
| 64 | + # Add all points to dataframe |
| 65 | + for i, (x, y) in enumerate(points): |
| 66 | + bar_rows.append({"x": x, "y": y, "direction": direction, "bar_id": bar_id, "order": i}) |
| 67 | + |
| 68 | + bar_id += 1 |
| 69 | + |
| 70 | +bar_df = pd.DataFrame(bar_rows) |
| 71 | + |
| 72 | +# Create circular gridlines (concentric circles at magnitude intervals) |
| 73 | +grid_rows = [] |
| 74 | +grid_angles = np.linspace(0, 2 * np.pi, 101) |
| 75 | +max_radius = max(frequencies) + 5 |
| 76 | +grid_radii = [5, 10, 15, 20, 25] |
| 77 | + |
| 78 | +for radius in grid_radii: |
| 79 | + if radius <= max_radius: |
| 80 | + for angle in grid_angles: |
| 81 | + grid_rows.append({"x": radius * np.cos(angle), "y": radius * np.sin(angle), "radius": radius}) |
| 82 | + |
| 83 | +grid_df = pd.DataFrame(grid_rows) |
| 84 | + |
| 85 | +# Create radial spokes (8 compass directions) |
| 86 | +spoke_rows = [] |
| 87 | +for deg in direction_angles: |
| 88 | + angle = np.radians(90 - deg) |
| 89 | + spoke_rows.append( |
| 90 | + {"x1": 0, "y1": 0, "x2": (max_radius + 2) * np.cos(angle), "y2": (max_radius + 2) * np.sin(angle)} |
| 91 | + ) |
| 92 | + |
| 93 | +spoke_df = pd.DataFrame(spoke_rows) |
| 94 | + |
| 95 | +# Create compass direction labels |
| 96 | +label_rows = [] |
| 97 | +label_radius = max_radius + 5 |
| 98 | +for deg, lbl in zip(direction_angles, directions, strict=True): |
| 99 | + angle = np.radians(90 - deg) |
| 100 | + label_rows.append({"label": lbl, "x": label_radius * np.cos(angle), "y": label_radius * np.sin(angle)}) |
| 101 | + |
| 102 | +label_df = pd.DataFrame(label_rows) |
| 103 | + |
| 104 | +# Create radius labels (frequency values) - positioned along NNE axis |
| 105 | +radius_labels = [] |
| 106 | +label_angle = np.radians(90 - 22.5) # NNE direction |
| 107 | +for r in [5, 10, 15, 20]: |
| 108 | + if r <= max_radius: |
| 109 | + radius_labels.append({"label": f"{r}", "x": r * np.cos(label_angle) + 1.5, "y": r * np.sin(label_angle)}) |
| 110 | + |
| 111 | +radius_label_df = pd.DataFrame(radius_labels) |
| 112 | + |
| 113 | +# Color palette - alternating Python Blue and Yellow |
| 114 | +colors = { |
| 115 | + "N": "#306998", |
| 116 | + "NE": "#FFD43B", |
| 117 | + "E": "#306998", |
| 118 | + "SE": "#FFD43B", |
| 119 | + "S": "#306998", |
| 120 | + "SW": "#FFD43B", |
| 121 | + "W": "#306998", |
| 122 | + "NW": "#FFD43B", |
| 123 | +} |
| 124 | + |
| 125 | +# Plot |
| 126 | +plot = ( |
| 127 | + ggplot() |
| 128 | + # Circular gridlines (frequency circles) |
| 129 | + + geom_path( |
| 130 | + aes(x="x", y="y", group="radius"), data=grid_df, color="#CCCCCC", size=0.5, alpha=0.5, linetype="dashed" |
| 131 | + ) |
| 132 | + # Radial spokes (direction lines) |
| 133 | + + geom_segment(aes(x="x1", y="y1", xend="x2", yend="y2"), data=spoke_df, color="#CCCCCC", size=0.5, alpha=0.5) |
| 134 | + # Bar wedges (wind rose bars) |
| 135 | + + geom_polygon( |
| 136 | + aes(x="x", y="y", group="bar_id", fill="direction"), |
| 137 | + data=bar_df, |
| 138 | + color="#333333", |
| 139 | + size=0.5, |
| 140 | + alpha=0.85, |
| 141 | + show_legend=False, |
| 142 | + ) |
| 143 | + # Compass direction labels |
| 144 | + + geom_text(aes(x="x", y="y", label="label"), data=label_df, size=16, color="#333333", fontweight="bold") |
| 145 | + # Frequency labels |
| 146 | + + geom_text(aes(x="x", y="y", label="label"), data=radius_label_df, size=10, color="#666666", ha="left") |
| 147 | + # Custom colors for directions |
| 148 | + + scale_fill_manual(values=colors) |
| 149 | + # Equal coordinate system for proper circles |
| 150 | + + coord_fixed(ratio=1) |
| 151 | + # Axis scaling with padding |
| 152 | + + scale_x_continuous(limits=(-35, 35)) |
| 153 | + + scale_y_continuous(limits=(-35, 35)) |
| 154 | + # Title |
| 155 | + + labs(title="Wind Direction Frequency · polar-bar · plotnine · pyplots.ai") |
| 156 | + # Clean polar-style theme |
| 157 | + + theme( |
| 158 | + figure_size=(12, 12), |
| 159 | + plot_title=element_text(size=24, ha="center"), |
| 160 | + axis_title=element_blank(), |
| 161 | + axis_text=element_blank(), |
| 162 | + axis_ticks=element_blank(), |
| 163 | + axis_line=element_blank(), |
| 164 | + panel_grid_major=element_blank(), |
| 165 | + panel_grid_minor=element_blank(), |
| 166 | + panel_background=element_blank(), |
| 167 | + plot_background=element_blank(), |
| 168 | + ) |
| 169 | +) |
| 170 | + |
| 171 | +# Save |
| 172 | +plot.save("plot.png", dpi=300, verbose=False) |
0 commit comments