{"spec_id":"windbarb-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nwindbarb-basic: Wind Barb Plot for Meteorological Data\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-19\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# This file is named plotnine.py, which shadows the plotnine library.\n# Remove the script directory before importing the library.\n_here = os.path.dirname(os.path.abspath(__file__))\nif _here in sys.path:\n    sys.path.remove(_here)\ndel _here\n\n_pn = importlib.import_module(\"plotnine\")\naes = _pn.aes\nannotate = _pn.annotate\nelement_line = _pn.element_line\nelement_rect = _pn.element_rect\nelement_text = _pn.element_text\ngeom_point = _pn.geom_point\ngeom_polygon = _pn.geom_polygon\ngeom_segment = _pn.geom_segment\nggplot = _pn.ggplot\nlabs = _pn.labs\nscale_x_continuous = _pn.scale_x_continuous\nscale_y_continuous = _pn.scale_y_continuous\ntheme = _pn.theme\ntheme_minimal = _pn.theme_minimal\ndel _pn\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1\n\n# Data - Surface wind observations from a grid of weather stations\nnp.random.seed(42)\n\n# Create a grid of observation points (6x5 grid = 30 stations)\nx_coords = np.linspace(0, 10, 6)\ny_coords = np.linspace(0, 8, 5)\nxx, yy = np.meshgrid(x_coords, y_coords)\nx = xx.flatten()\ny = yy.flatten()\n\n# Generate wind components (u: east-west, v: north-south) in knots\nu = np.random.uniform(-30, 30, len(x))\nv = np.random.uniform(-25, 25, len(x))\n\n# Force calm conditions (< 2.5 knots)\nu[0] = 0.5\nv[0] = 0.3\nu[5] = 1.0\nv[5] = -0.8\n\n# Force high winds with pennants (50+ knots)\nu[15] = 45\nv[15] = 35\nu[20] = 55\nv[20] = 10\n\n# Calculate wind speed and direction\nwind_speed = np.sqrt(u**2 + v**2)\n# Direction FROM which wind blows (meteorological convention)\nwind_direction_rad = np.arctan2(-u, -v)\n\n# Build wind barb components: staffs, barb flags, pennants, calm circles\nstaff_records = []\nbarb_records = []\npennant_records = []\ncalm_records = []\n\nscale = 0.06  # Scale factor for staff length\n\nfor i in range(len(x)):\n    speed = wind_speed[i]\n\n    if speed < 2.5:\n        calm_records.append({\"x\": x[i], \"y\": y[i], \"speed\": speed})\n    else:\n        dir_rad = wind_direction_rad[i]\n        ux = -np.sin(dir_rad)\n        uy = -np.cos(dir_rad)\n\n        staff_len = min(speed * scale, 2.5)\n        x2 = x[i] + ux * staff_len\n        y2 = y[i] + uy * staff_len\n        staff_records.append({\"x\": x[i], \"y\": y[i], \"xend\": x2, \"yend\": y2, \"speed\": speed})\n\n        # Perpendicular vector for barb flags (left side, Northern Hemisphere)\n        px = -uy\n        py = ux\n\n        remaining_speed = speed\n        barb_pos = 0.85\n        barb_idx = 0\n\n        # Pennants (50 knots each)\n        pennant_id = 0\n        while remaining_speed >= 50 and barb_idx < 3:\n            pos_factor = barb_pos - barb_idx * 0.15\n            bx = x[i] + ux * staff_len * pos_factor\n            by = y[i] + uy * staff_len * pos_factor\n\n            tri_base = 0.25\n            tri_height = 0.35\n            tip_x = bx + px * tri_height\n            tip_y = by + py * tri_height\n            base1_x = bx - ux * tri_base / 2\n            base1_y = by - uy * tri_base / 2\n            base2_x = bx + ux * tri_base / 2\n            base2_y = by + uy * tri_base / 2\n\n            group_id = f\"{i}_{pennant_id}\"\n            pennant_records.append({\"x\": base1_x, \"y\": base1_y, \"group\": group_id, \"order\": 1})\n            pennant_records.append({\"x\": tip_x, \"y\": tip_y, \"group\": group_id, \"order\": 2})\n            pennant_records.append({\"x\": base2_x, \"y\": base2_y, \"group\": group_id, \"order\": 3})\n\n            remaining_speed -= 50\n            barb_idx += 1\n            pennant_id += 1\n\n        # Full barbs (10 knots each)\n        while remaining_speed >= 10 and barb_idx < 8:\n            pos_factor = barb_pos - barb_idx * 0.12\n            bx = x[i] + ux * staff_len * pos_factor\n            by = y[i] + uy * staff_len * pos_factor\n            barb_len = 0.40\n            barb_records.append(\n                {\"x\": bx, \"y\": by, \"xend\": bx + px * barb_len, \"yend\": by + py * barb_len, \"type\": \"full\"}\n            )\n            remaining_speed -= 10\n            barb_idx += 1\n\n        # Half barb (5 knots)\n        if remaining_speed >= 5:\n            pos_factor = barb_pos - barb_idx * 0.12\n            bx = x[i] + ux * staff_len * pos_factor\n            by = y[i] + uy * staff_len * pos_factor\n            barb_len = 0.22\n            barb_records.append(\n                {\"x\": bx, \"y\": by, \"xend\": bx + px * barb_len, \"yend\": by + py * barb_len, \"type\": \"half\"}\n            )\n\n# Create DataFrames\nstaff_df = pd.DataFrame(staff_records) if staff_records else pd.DataFrame(columns=[\"x\", \"y\", \"xend\", \"yend\", \"speed\"])\nbarb_df = pd.DataFrame(barb_records) if barb_records else pd.DataFrame(columns=[\"x\", \"y\", \"xend\", \"yend\", \"type\"])\npennant_df = pd.DataFrame(pennant_records) if pennant_records else pd.DataFrame(columns=[\"x\", \"y\", \"group\", \"order\"])\ncalm_df = pd.DataFrame(calm_records) if calm_records else pd.DataFrame(columns=[\"x\", \"y\", \"speed\"])\n\n# Plot - layer composition using grammar of graphics\nplot = (\n    ggplot()\n    + geom_segment(data=staff_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=BRAND, size=1.5)\n    + geom_point(data=staff_df, mapping=aes(x=\"x\", y=\"y\"), color=BRAND, size=3)\n)\n\nif len(barb_df) > 0:\n    plot = plot + geom_segment(data=barb_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=BRAND, size=1.5)\n\nif len(pennant_df) > 0:\n    plot = plot + geom_polygon(\n        data=pennant_df, mapping=aes(x=\"x\", y=\"y\", group=\"group\"), fill=BRAND, color=BRAND, size=0.5\n    )\n\nif len(calm_df) > 0:\n    plot = plot + geom_point(data=calm_df, mapping=aes(x=\"x\", y=\"y\"), color=BRAND, fill=PAGE_BG, size=6, stroke=1.5)\n\n# Legend annotation positioned in the right margin outside the data area\nlegend_text = \"Wind Barb Key:\\n○  Calm (< 2.5 kt)\\n╲  Half barb = 5 kt\\n╲╲ Full barb = 10 kt\\n▲  Pennant = 50 kt\"\n\nplot = (\n    plot\n    + annotate(\n        \"label\",\n        x=13.8,\n        y=4.0,\n        label=legend_text,\n        size=11,\n        ha=\"right\",\n        va=\"center\",\n        fill=ELEVATED_BG,\n        alpha=0.95,\n        label_padding=0.5,\n        color=INK_SOFT,\n    )\n    + scale_x_continuous(limits=(-0.5, 14))\n    + scale_y_continuous(limits=(-0.5, 8.5))\n    + labs(x=\"Longitude (°E)\", y=\"Latitude (°N)\", title=\"windbarb-basic · python · plotnine · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        figure_size=(16, 9),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_border=element_rect(color=INK_SOFT, fill=None),\n        text=element_text(size=14, color=INK),\n        axis_title=element_text(size=20, color=INK),\n        axis_text=element_text(size=16, color=INK_SOFT),\n        plot_title=element_text(size=24, color=INK),\n        panel_grid_major=element_line(color=INK_SOFT, size=0.3, alpha=0.12),\n        panel_grid_minor=element_line(color=INK_SOFT, size=0.15, alpha=0.06),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}