{"spec_id":"windbarb-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nwindbarb-basic: Wind Barb Plot for Meteorological Data\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1 — used for calm circles\n\n# Data — synthetic wind observations from a 6×5 grid of weather stations\nnp.random.seed(42)\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()\nu = np.random.uniform(-30, 30, len(x))\nv = np.random.uniform(-25, 25, len(x))\n\n# Force calm conditions and high-wind (pennant) cases for feature coverage\nu[0], v[0] = 0.5, 0.3\nu[5], v[5] = 1.0, -0.8\nu[15], v[15] = 45.0, 35.0  # ~57 kt — one pennant + half barb\nu[20], v[20] = 55.0, 10.0  # ~56 kt — one pennant + half barb\n\nwind_speed = np.sqrt(u**2 + v**2)\nwind_direction = np.degrees(np.arctan2(-u, -v)) % 360  # FROM direction (meteorological)\n\ndf = pd.DataFrame({\"x\": x, \"y\": y, \"u\": u, \"v\": v, \"speed\": wind_speed, \"direction\": wind_direction})\ncalm_df = df[df[\"speed\"] < 2.5].copy()\nbarbed_df = df[df[\"speed\"] >= 2.5].copy()\n\n# Staff: line from station toward wind source\nSCALE = 0.03\nbarbed_df = barbed_df.copy()\nbarbed_df[\"x2\"] = barbed_df[\"x\"] - barbed_df[\"u\"] * SCALE\nbarbed_df[\"y2\"] = barbed_df[\"y\"] - barbed_df[\"v\"] * SCALE\n\n# Build barb segments and pennant markers per station\nbarb_records = []\npennant_records = []\n\nfor _, row in barbed_df.iterrows():\n    spd = row[\"speed\"]\n    direction_rad = np.radians(row[\"direction\"])\n    ux_s = np.sin(direction_rad)  # unit vector toward wind source\n    uy_s = np.cos(direction_rad)\n    px_s = -uy_s  # left-perpendicular (Northern Hemisphere convention)\n    py_s = ux_s\n\n    staff_len = spd * SCALE\n    remaining = spd\n    pos_offset = 0.0\n\n    num_pennants = int(remaining // 50)\n    for _ in range(min(num_pennants, 3)):\n        bpos = 0.85 - pos_offset\n        bx = row[\"x\"] + ux_s * staff_len * bpos\n        by = row[\"y\"] + uy_s * staff_len * bpos\n        pennant_records.append({\"x\": bx, \"y\": by, \"angle\": (row[\"direction\"] + 270) % 360, \"speed\": spd})\n        pos_offset += 0.20\n    remaining -= num_pennants * 50\n\n    num_full = int(remaining // 10)\n    for _ in range(min(num_full, 5)):\n        bpos = 0.85 - pos_offset\n        bx = row[\"x\"] + ux_s * staff_len * bpos\n        by = row[\"y\"] + uy_s * staff_len * bpos\n        barb_records.append({\"x\": bx, \"y\": by, \"x2\": bx + px_s * 0.35, \"y2\": by + py_s * 0.35, \"speed\": spd})\n        pos_offset += 0.15\n    remaining -= num_full * 10\n\n    if remaining >= 5:\n        bpos = 0.85 - pos_offset\n        bx = row[\"x\"] + ux_s * staff_len * bpos\n        by = row[\"y\"] + uy_s * staff_len * bpos\n        barb_records.append({\"x\": bx, \"y\": by, \"x2\": bx + px_s * 0.18, \"y2\": by + py_s * 0.18, \"speed\": spd})\n\nbarb_df = pd.DataFrame(barb_records) if barb_records else pd.DataFrame(columns=[\"x\", \"y\", \"x2\", \"y2\", \"speed\"])\npennant_df = pd.DataFrame(pennant_records) if pennant_records else pd.DataFrame(columns=[\"x\", \"y\", \"angle\", \"speed\"])\n\n# Continuous speed color encoding — cividis for CVD-safe sequential scale\nspeed_color = alt.Color(\n    \"speed:Q\",\n    scale=alt.Scale(scheme=\"cividis\", domainMin=0, domainMax=60),\n    legend=alt.Legend(title=\"Wind Speed (kt)\", orient=\"bottom-right\"),\n)\n\n# Layer 1: Staff lines\nstaff = (\n    alt.Chart(barbed_df)\n    .mark_rule(strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"x:Q\", title=\"Longitude (°E)\", scale=alt.Scale(domain=[-0.5, 11])),\n        y=alt.Y(\"y:Q\", title=\"Latitude (°N)\", scale=alt.Scale(domain=[-0.8, 9])),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=speed_color,\n        tooltip=[\n            alt.Tooltip(\"x:Q\", title=\"Longitude\", format=\".1f\"),\n            alt.Tooltip(\"y:Q\", title=\"Latitude\", format=\".1f\"),\n            alt.Tooltip(\"speed:Q\", title=\"Wind Speed (kt)\", format=\".1f\"),\n            alt.Tooltip(\"direction:Q\", title=\"Direction (° from N)\", format=\".0f\"),\n        ],\n    )\n)\n\n# Layer 2: Barb line segments (full and half barbs)\nif len(barb_df) > 0:\n    barbs = (\n        alt.Chart(barb_df).mark_rule(strokeWidth=2.5).encode(x=\"x:Q\", y=\"y:Q\", x2=\"x2:Q\", y2=\"y2:Q\", color=speed_color)\n    )\nelse:\n    barbs = alt.Chart(pd.DataFrame({\"x\": []})).mark_point()\n\n# Layer 3: Pennant markers — filled triangles oriented by wind direction\nif len(pennant_df) > 0:\n    pennants = (\n        alt.Chart(pennant_df)\n        .mark_point(shape=\"triangle\", filled=True, size=700)\n        .encode(\n            x=\"x:Q\",\n            y=\"y:Q\",\n            angle=alt.Angle(\"angle:Q\", scale=alt.Scale(domain=[0, 360], range=[0, 360])),\n            color=speed_color,\n        )\n    )\nelse:\n    pennants = alt.Chart(pd.DataFrame({\"x\": []})).mark_point()\n\n# Layer 4: Calm wind indicators — open circles (< 2.5 kt)\ncalm_circles = (\n    alt.Chart(calm_df)\n    .mark_point(shape=\"circle\", filled=False, size=350, stroke=BRAND, strokeWidth=4)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        tooltip=[\n            alt.Tooltip(\"x:Q\", title=\"Longitude\", format=\".1f\"),\n            alt.Tooltip(\"y:Q\", title=\"Latitude\", format=\".1f\"),\n            alt.Tooltip(\"speed:Q\", title=\"Wind Speed (kt)\", format=\".2f\"),\n        ],\n    )\n)\n\n# Chart assembly with theme-adaptive chrome\nchart = (\n    alt.layer(staff, barbs, pennants, calm_circles)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\n            text=\"windbarb-basic · python · altair · anyplot.ai\",\n            subtitle=\"Half barb = 5 kt  |  Full barb = 10 kt  |  ▲ Pennant = 50 kt  |  ○ Calm < 2.5 kt\",\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=\"transparent\")\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK, subtitleColor=INK_SOFT, fontSize=28, subtitleFontSize=20)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}