{"spec_id":"windrose-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nwindrose-basic: Wind Rose Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-08-05\n\"\"\"\n\nimport math\nimport os\nimport sys\n\nimport numpy as np\n\n\n# Avoid import shadowing: remove script directory and cwd from path\n_script_dir = os.path.dirname(os.path.abspath(__file__))\n_cwd = os.getcwd()\nsys.path = [p for p in sys.path if os.path.abspath(p) not in (_script_dir, _cwd, \"\")]\n\nimport pygal\nfrom pygal.style import Style\n\n\n# Restore path for later operations\nsys.path.insert(0, _cwd)\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette (canonical order)\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data generation\nnp.random.seed(42)\nn_observations = 8760  # ~1 year of hourly measurements\n\n# Simulate prevailing winds from SW (225°) and W (270°) with variation\ndirections = np.concatenate(\n    [\n        np.random.normal(225, 30, int(n_observations * 0.35)),  # SW dominant\n        np.random.normal(270, 25, int(n_observations * 0.25)),  # W secondary\n        np.random.normal(180, 40, int(n_observations * 0.15)),  # S occasional\n        np.random.uniform(0, 360, int(n_observations * 0.25)),  # Random variation\n    ]\n)\ndirections = directions % 360  # Normalize to 0-360\n\n# Wind speeds drawn from an exponential distribution per direction cluster\n# (calm sectors decay faster, gustier sectors carry a longer tail)\nspeeds = np.concatenate(\n    [\n        np.random.exponential(6.0, int(n_observations * 0.35)),  # SW: moderate-strong\n        np.random.exponential(7.0, int(n_observations * 0.25)),  # W: stronger, longer tail\n        np.random.exponential(4.0, int(n_observations * 0.15)),  # S: lighter\n        np.random.exponential(3.0, int(n_observations * 0.25)),  # Others: light\n    ]\n)\n\n# Define 8 direction sectors. pygal's Radar places category index 0 at the\n# top and lays out subsequent categories COUNTER-clockwise, so the labels\n# must be listed counter-clockwise-in-degrees (N, then 315, 270, ...) for the\n# rendered spokes to match true (clockwise) compass bearing.\ndirection_labels = [\"N\", \"NW\", \"W\", \"SW\", \"S\", \"SE\", \"E\", \"NE\"]\n\n# Define wind speed ranges (m/s)\nspeed_bins = [0, 5, 10, 15, np.inf]\nspeed_labels = [\"0-5 m/s\", \"5-10 m/s\", \"10-15 m/s\", \"15+ m/s\"]\n\n# Calculate frequencies for each direction and speed bin\nfrequencies = {label: [] for label in speed_labels}\n\nfor dir_center in [0, 315, 270, 225, 180, 135, 90, 45]:\n    if dir_center == 0:\n        # North spans 337.5-360 and 0-22.5\n        mask = (directions >= 337.5) | (directions < 22.5)\n    else:\n        low = dir_center - 22.5\n        high = dir_center + 22.5\n        mask = (directions >= low) & (directions < high)\n\n    dir_speeds = speeds[mask]\n\n    # Count frequencies in each speed bin\n    for j, (low_speed, high_speed) in enumerate(zip(speed_bins[:-1], speed_bins[1:], strict=True)):\n        count = np.sum((dir_speeds >= low_speed) & (dir_speeds < high_speed))\n        freq_pct = (count / len(directions)) * 100\n        frequencies[speed_labels[j]].append(round(freq_pct, 2))\n\n# Build cumulative values for proper stacked rendering\ncumulative = {}\nfor i, label in enumerate(speed_labels):\n    cumulative[label] = [sum(frequencies[speed_labels[k]][j] for k in range(i + 1)) for j in range(8)]\n\n# Round the radial max up to a clean multiple of 5 for a tidier axis\nradial_max = max(cumulative[speed_labels[-1]])\nradial_max = math.ceil(radial_max / 5) * 5\n\n# Custom style — sizing tuned for the 2400x2400 square canvas (see\n# prompts/library/pygal.md \"Sizing + Theme for 3200x1800 px\"; same pixel\n# area as the square format, so the same unitless values apply)\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n    opacity=0.95,\n    guide_stroke_width=1,\n)\n\n# Create radar chart (wind rose)\nchart = pygal.Radar(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    title=\"windrose-basic · python · pygal · anyplot.ai\",\n    y_title=\"Frequency (%)\",\n    show_legend=True,\n    legend_at_bottom=False,\n    legend_box_size=40,\n    fill=True,\n    stroke=True,\n    show_dots=False,\n    inner_radius=0.05,\n    truncate_legend=-1,\n    margin=90,\n    spacing=30,\n    show_y_guides=True,\n    show_x_guides=False,\n    range=(0, radial_max),\n)\n\n# Set direction labels\nchart.x_labels = direction_labels\n\n# Add series from strongest to calmest (drawing order)\n# This creates proper visual stacking with each layer visible\nreversed_labels = list(reversed(speed_labels))  # [\"15+ m/s\", \"10-15 m/s\", ...]\nfor label in reversed_labels:\n    chart.add(label, cumulative[label])\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}