{"spec_id":"windbarb-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nwindbarb-basic: Wind Barb Plot for Meteorological Data\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_polygon,\n    geom_segment,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID_COLOR = \"#CCCCCC\" if THEME == \"light\" else \"#3D3D3A\"\nBRAND = \"#009E73\"\n\nnp.random.seed(42)\n\n# Generate grid of weather stations (8x6 grid = 48 stations)\nn_x, n_y = 8, 6\nx_coords = np.linspace(0, 140, n_x)\ny_coords = np.linspace(0, 100, n_y)\nX, Y = np.meshgrid(x_coords, y_coords)\nx = X.flatten()\ny = Y.flatten()\n\n# Generate wind components (u = east-west, v = north-south) in knots\nu = 15 * np.sin(x / 30) + np.random.normal(0, 3, len(x))\nv = 10 * np.cos(y / 20) + np.random.normal(0, 3, len(y))\n\n# Force calm winds at two stations (< 2.5 knots) for demonstration\ncalm_indices = [0, 5]\nu[calm_indices] = 0.5\nv[calm_indices] = 0.5\n\n# Force high-speed winds at two stations (~54 knots) to demonstrate pennants\nhigh_indices = [20, 21]\nu[high_indices] = 45.0\nv[high_indices] = 30.0\n\nwind_speed = np.sqrt(u**2 + v**2)\n\n# Wind barb geometry parameters\nbarb_length = 8\nbarb_spacing = 1.5\nbarb_tick_length = 2.5\n\n# Build barb segments and pennant polygons for each station\nsegments = []\npennants = []\n\nfor i in range(len(x)):\n    speed = np.sqrt(u[i] ** 2 + v[i] ** 2)\n    angle = np.arctan2(-v[i], -u[i])\n\n    if speed < 2.5:\n        continue\n\n    cos_a, sin_a = np.cos(angle), np.sin(angle)\n    x_end = x[i] + barb_length * cos_a\n    y_end = y[i] + barb_length * sin_a\n\n    # Main staff\n    segments.append({\"x\": x[i], \"y\": y[i], \"xend\": x_end, \"yend\": y_end})\n\n    remaining_speed = speed\n    barb_position = barb_length - 0.5\n    perp_angle = angle + np.pi / 2\n\n    # Pennants: 50 knots each (filled triangles)\n    while remaining_speed >= 47.5:\n        bx = x[i] + barb_position * cos_a\n        by = y[i] + barb_position * sin_a\n        tip_x = bx + barb_tick_length * np.cos(perp_angle)\n        tip_y = by + barb_tick_length * np.sin(perp_angle)\n        base_x = x[i] + (barb_position - barb_spacing) * cos_a\n        base_y = y[i] + (barb_position - barb_spacing) * sin_a\n        pennants.append({\"x\": [bx, tip_x, base_x], \"y\": [by, tip_y, base_y]})\n        remaining_speed -= 50\n        barb_position -= barb_spacing\n\n    # Full barbs: 10 knots each\n    while remaining_speed >= 7.5:\n        bx = x[i] + barb_position * cos_a\n        by = y[i] + barb_position * sin_a\n        segments.append(\n            {\n                \"x\": bx,\n                \"y\": by,\n                \"xend\": bx + barb_tick_length * np.cos(perp_angle),\n                \"yend\": by + barb_tick_length * np.sin(perp_angle),\n            }\n        )\n        remaining_speed -= 10\n        barb_position -= barb_spacing\n\n    # Half barb: 5 knots\n    if remaining_speed >= 2.5:\n        bx = x[i] + barb_position * cos_a\n        by = y[i] + barb_position * sin_a\n        segments.append(\n            {\n                \"x\": bx,\n                \"y\": by,\n                \"xend\": bx + barb_tick_length * 0.5 * np.cos(perp_angle),\n                \"yend\": by + barb_tick_length * 0.5 * np.sin(perp_angle),\n            }\n        )\n\nsegment_df = pd.DataFrame(segments)\nstation_df = pd.DataFrame({\"x\": x, \"y\": y, \"speed\": wind_speed})\ncalm_df = station_df[station_df[\"speed\"] < 2.5].copy()\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=GRID_COLOR, size=0.3),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=24),\n)\n\nplot = ggplot() + theme_minimal() + anyplot_theme\n\n# Staffs and barb ticks\nif len(segment_df) > 0:\n    plot = plot + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=segment_df, color=BRAND, size=1.2)\n\n# Pennants as filled triangles\nfor pennant in pennants:\n    pennant_df = pd.DataFrame({\"x\": pennant[\"x\"], \"y\": pennant[\"y\"]})\n    plot = plot + geom_polygon(aes(x=\"x\", y=\"y\"), data=pennant_df, fill=BRAND, color=BRAND, size=0.5)\n\n# Calm wind stations (open circles for speed < 2.5 knots)\nif len(calm_df) > 0:\n    plot = plot + geom_point(aes(x=\"x\", y=\"y\"), data=calm_df, shape=1, size=6, color=BRAND, stroke=1.5)\n\n# Station center dots\nplot = plot + geom_point(aes(x=\"x\", y=\"y\"), data=station_df, size=2, color=BRAND)\n\nplot = (\n    plot\n    + labs(x=\"Longitude (°E)\", y=\"Latitude (°N)\", title=\"windbarb-basic · python · letsplot · anyplot.ai\")\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(expand=[0.1, 0])\n    + scale_y_continuous(expand=[0.1, 0])\n    + ggsize(1600, 900)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}