{"spec_id":"windrose-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nwindrose-basic: Wind Rose Chart\nLibrary: matplotlib 3.11.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path = [p for p in sys.path if p not in (\"\", \".\", os.path.dirname(os.path.abspath(__file__)))]\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette for wind speed bins (starting with brand green)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - Simulated annual wind measurements (8760 hourly readings)\nnp.random.seed(42)\n\nn_observations = 8760  # One year of hourly data\n\n# Generate realistic wind direction data with prevailing westerly winds\n# Using mixture of normal distributions wrapped to [0, 360)\ndirections_main = np.random.normal(240, 30, int(n_observations * 0.5))  # SW prevailing\ndirections_secondary = np.random.normal(315, 25, int(n_observations * 0.3))  # NW secondary\ndirections_random = np.random.uniform(0, 360, int(n_observations * 0.2))  # Random\n\ndirections = np.concatenate([directions_main, directions_secondary, directions_random])\ndirections = directions % 360  # Wrap to [0, 360)\n\n# Wind speeds using Weibull distribution (common for wind data)\n# Scale=7.5 keeps the mean around 6.6 m/s (typical moderate wind-climate) while\n# giving the 15+ m/s tail bin ~1% of observations so it stays visible in the rose\nspeeds = np.random.weibull(2.2, len(directions)) * 7.5\nspeeds = np.clip(speeds, 0, 25)\n\n# Define bins - 16 direction sectors (22.5 degrees each)\nn_dir_bins = 16\ndir_bin_width = 360 / n_dir_bins\ndirection_centers = np.radians(np.arange(0, 360, dir_bin_width))\n\n# Speed bins in m/s\nspeed_bins = [0, 3, 6, 9, 12, 15, 25]\nspeed_labels = [\"0-3\", \"3-6\", \"6-9\", \"9-12\", \"12-15\", \"15+\"]\n\n# Calculate frequencies for each direction/speed combination\nfreq_matrix = np.zeros((n_dir_bins, len(speed_bins) - 1))\n\nfor i in range(n_dir_bins):\n    # Calculate bin edges, centered on the direction\n    bin_center = i * dir_bin_width\n    bin_low = (bin_center - dir_bin_width / 2) % 360\n    bin_high = (bin_center + dir_bin_width / 2) % 360\n\n    # Handle wrap-around at 0/360 degrees\n    if bin_low > bin_high:\n        dir_mask = (directions >= bin_low) | (directions < bin_high)\n    else:\n        dir_mask = (directions >= bin_low) & (directions < bin_high)\n\n    dir_speeds = speeds[dir_mask]\n\n    for j in range(len(speed_bins) - 1):\n        speed_mask = (dir_speeds >= speed_bins[j]) & (dir_speeds < speed_bins[j + 1])\n        freq_matrix[i, j] = np.sum(speed_mask)\n\n# Convert to percentage\nfreq_matrix = freq_matrix / len(directions) * 100\n\n# Plot - square canonical canvas (6in x 6in @ 400dpi -> 2400x2400px) for radial symmetry\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, subplot_kw={\"projection\": \"polar\"}, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nfig.subplots_adjust(left=0.06, right=0.66, top=0.86, bottom=0.06)\n\n# Bar width slightly less than bin width for visual clarity\nbar_width = np.radians(20)\n\n# Stack the bars for each speed category\nbottoms = np.zeros(n_dir_bins)\n\nfor j in range(len(speed_bins) - 1):\n    ax.bar(\n        direction_centers,\n        freq_matrix[:, j],\n        width=bar_width,\n        bottom=bottoms,\n        color=IMPRINT[j],\n        edgecolor=PAGE_BG,\n        linewidth=0.4,\n        label=f\"{speed_labels[j]} m/s\",\n    )\n    bottoms += freq_matrix[:, j]\n\n# Configure polar plot - North at top, clockwise direction (meteorological convention)\nax.set_theta_zero_location(\"N\")\nax.set_theta_direction(-1)\n\n# Direction labels for 16 sectors\ndirection_labels = [\"N\", \"NNE\", \"NE\", \"ENE\", \"E\", \"ESE\", \"SE\", \"SSE\", \"S\", \"SSW\", \"SW\", \"WSW\", \"W\", \"WNW\", \"NW\", \"NNW\"]\nax.set_xticks(np.radians(np.arange(0, 360, 22.5)))\nax.set_xticklabels(direction_labels, fontsize=10, fontweight=\"bold\", color=INK)\n\n# Radial axis - frequency percentage\nmax_freq = np.ceil(bottoms.max() * 1.1)\nax.set_ylim(0, max_freq)\nyticks = np.arange(0, max_freq + 1, 2)\nax.set_yticks(yticks)\nax.set_yticklabels([f\"{int(y)}%\" for y in yticks], fontsize=8, color=INK_SOFT)\n\n# Grid styling - subtle, solid lines\nax.grid(True, alpha=0.15, linestyle=\"-\", color=INK_SOFT, linewidth=0.5)\n\n# Spine styling\nax.spines[\"polar\"].set_color(INK_SOFT)\nax.spines[\"polar\"].set_linewidth(0.4)\n\n# Title\nax.set_title(\n    \"windrose-basic · python · matplotlib · anyplot.ai\", fontsize=10, fontweight=\"medium\", color=INK, pad=16, loc=\"left\"\n)\n\n# Legend - positioned in the right margin reserved by subplots_adjust\nleg = ax.legend(\n    title=\"Wind Speed\",\n    title_fontsize=10,\n    fontsize=8,\n    loc=\"upper left\",\n    bbox_to_anchor=(0.68, 0.94),\n    bbox_transform=fig.transFigure,\n    framealpha=0.95,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\nif leg:\n    plt.setp(leg.get_title(), color=INK)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}