{"spec_id":"windbarb-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// windbarb-basic: Wind Barb Plot for Meteorological Data\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Synthetic surface-station grid over a regional domain, with a low-pressure\n// vortex (Rankine profile, counterclockwise / cyclonic — Northern Hemisphere)\n// centred on one of the grid points so the demo also exercises the calm case.\nconst LONS = [0, 2, 4, 6, 8, 10];\nconst LATS = [40, 42, 44, 46, 48];\nconst CENTER = { lon: 4, lat: 44 };\nconst R_MAX = 3; // radius (deg) of peak tangential wind\nconst V_MAX = 65; // peak tangential speed (knots) — exceeds 50kt so a pennant appears\n\nconst stations = [];\nfor (const lat of LATS) {\n  for (const lon of LONS) {\n    const dx = lon - CENTER.lon;\n    const dy = lat - CENTER.lat;\n    const r = Math.sqrt(dx * dx + dy * dy);\n    let u = 0;\n    let v = 0;\n    if (r > 1e-9) {\n      const speed = r <= R_MAX ? (V_MAX * r) / R_MAX : (V_MAX * R_MAX) / r;\n      // Tangential (counterclockwise) unit vector around the vortex center.\n      const tx = -dy / r;\n      const ty = dx / r;\n      u = speed * tx;\n      v = speed * ty;\n    }\n    stations.push({ x: lon, y: lat, u, v });\n  }\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Wind barb rendering plugin ----------------------------------------------\nconst STAFF_LEN = 65; // px, station to barb tip\nconst BARB_LEN = 20; // px, feather length\nconst BARB_SPACING = 13; // px, spacing between feathers along the staff\nconst PENNANT_WIDTH = 13; // px, base width of a 50kt pennant along the staff\nconst CALM_RADIUS = 6; // px, open circle for calm winds\n\nconst rotate = (v, deg) => {\n  const a = (deg * Math.PI) / 180;\n  const cos = Math.cos(a);\n  const sin = Math.sin(a);\n  return { x: v.x * cos - v.y * sin, y: v.x * sin + v.y * cos };\n};\n\nconst decomposeKnots = (speed) => {\n  let remaining = Math.round(speed / 5) * 5;\n  const pennants = Math.floor(remaining / 50);\n  remaining -= pennants * 50;\n  const fullBarbs = Math.floor(remaining / 10);\n  remaining -= fullBarbs * 10;\n  const halfBarb = remaining >= 5 ? 1 : 0;\n  return { pennants, fullBarbs, halfBarb };\n};\n\nconst windBarbPlugin = {\n  id: \"windBarbPlugin\",\n  afterDatasetsDraw(chart) {\n    const ctx = chart.ctx;\n    const meta = chart.getDatasetMeta(0);\n    const data = chart.data.datasets[0].data;\n\n    ctx.save();\n    ctx.strokeStyle = t.palette[0];\n    ctx.fillStyle = t.palette[0];\n    ctx.lineWidth = 2.5;\n    ctx.lineCap = \"round\";\n\n    data.forEach((point, i) => {\n      const cx = meta.data[i].x;\n      const cy = meta.data[i].y;\n      const speed = Math.sqrt(point.u * point.u + point.v * point.v);\n\n      // Calm winds: open circle, no staff (spec: < 2.5 knots).\n      if (speed < 2.5) {\n        ctx.beginPath();\n        ctx.arc(cx, cy, CALM_RADIUS, 0, 2 * Math.PI);\n        ctx.stroke();\n        return;\n      }\n\n      // Staff points FROM the direction the wind blows (opposite of the\n      // (u, v) motion vector). Canvas y grows downward, so a northward (+v)\n      // component maps to a negative screen dy.\n      const staffDir = { x: -point.u / speed, y: point.v / speed };\n      const tip = { x: cx + staffDir.x * STAFF_LEN, y: cy + staffDir.y * STAFF_LEN };\n\n      ctx.beginPath();\n      ctx.moveTo(cx, cy);\n      ctx.lineTo(tip.x, tip.y);\n      ctx.stroke();\n\n      // Feathers swept back from the staff, consistently on one side.\n      const barbDir = rotate(staffDir, 120);\n      const attach = (dist) => ({\n        x: cx + staffDir.x * dist,\n        y: cy + staffDir.y * dist,\n      });\n\n      const { pennants, fullBarbs, halfBarb } = decomposeKnots(speed);\n      let dist = STAFF_LEN;\n\n      for (let p = 0; p < pennants; p += 1) {\n        const base1 = attach(dist);\n        const base2 = attach(dist - PENNANT_WIDTH);\n        const mid = { x: (base1.x + base2.x) / 2, y: (base1.y + base2.y) / 2 };\n        const apex = { x: mid.x + barbDir.x * BARB_LEN, y: mid.y + barbDir.y * BARB_LEN };\n        ctx.beginPath();\n        ctx.moveTo(base1.x, base1.y);\n        ctx.lineTo(base2.x, base2.y);\n        ctx.lineTo(apex.x, apex.y);\n        ctx.closePath();\n        ctx.fill();\n        dist -= PENNANT_WIDTH;\n      }\n\n      for (let b = 0; b < fullBarbs; b += 1) {\n        const p0 = attach(dist);\n        const p1 = { x: p0.x + barbDir.x * BARB_LEN, y: p0.y + barbDir.y * BARB_LEN };\n        ctx.beginPath();\n        ctx.moveTo(p0.x, p0.y);\n        ctx.lineTo(p1.x, p1.y);\n        ctx.stroke();\n        dist -= BARB_SPACING;\n      }\n\n      if (halfBarb) {\n        const p0 = attach(dist);\n        const p1 = { x: p0.x + barbDir.x * BARB_LEN * 0.5, y: p0.y + barbDir.y * BARB_LEN * 0.5 };\n        ctx.beginPath();\n        ctx.moveTo(p0.x, p0.y);\n        ctx.lineTo(p1.x, p1.y);\n        ctx.stroke();\n      }\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Surface stations\",\n        data: stations,\n        pointRadius: 0,\n        showLine: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 24, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"windbarb-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -1.5,\n        max: 11.5,\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 2, callback: (v) => `${v}°E` },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Longitude\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: \"linear\",\n        min: 38.5,\n        max: 49.5,\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 2, callback: (v) => `${v}°N` },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Latitude\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [windBarbPlugin],\n});\n"}