{"spec_id":"windbarb-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// windbarb-basic: Wind Barb Plot for Meteorological Data\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Small fixed-seed LCG — no network, no Math.random(), fully reproducible.\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return function () {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\nconst rand = makeLcg(20260615);\n\n// Grid of surface weather stations over a synoptic map. Positions carry a\n// little jitter around a regular lattice so barbs stay legible (no overlap)\n// while looking like a real observation network rather than a perfect grid.\nconst COLS = 9;\nconst ROWS = 6;\nconst SPAN_X = 90; // degrees longitude, arbitrary reference frame\nconst SPAN_Y = 55; // degrees latitude\nconst cx = SPAN_X / 2;\nconst cy = SPAN_Y / 2;\n\nconst stations = [];\nfor (let row = 0; row < ROWS; row++) {\n  for (let col = 0; col < COLS; col++) {\n    const x = (col / (COLS - 1)) * SPAN_X + (rand() - 0.5) * 4;\n    const y = (row / (ROWS - 1)) * SPAN_Y + (rand() - 0.5) * 4;\n\n    // Synthetic flow: a cyclonic gyre near the map centre (low-pressure\n    // system) superposed on a background westerly, in u/v knots.\n    const dx = x - cx;\n    const dy = y - cy;\n    const r = Math.sqrt(dx * dx + dy * dy) + 1e-6;\n    const rotSpeed = 34 * Math.exp(-r / 24);\n    const u = (-dy / r) * rotSpeed + 7 + (rand() - 0.5) * 3; // eastward component\n    const v = (dx / r) * rotSpeed + (rand() - 0.5) * 3; // northward component\n\n    stations.push({ x, y, u, v });\n  }\n}\n// Force a couple of calm stations (< 2.5 kt) so the \"open circle\" notation\n// actually appears in the plot, per the specification's notes.\nstations[Math.floor(COLS * 1.5)].u = 0.5;\nstations[Math.floor(COLS * 1.5)].v = -0.3;\nstations[stations.length - Math.floor(COLS * 1.5)].u = -0.4;\nstations[stations.length - Math.floor(COLS * 1.5)].v = 0.6;\n// Force one station past 50 kt so the pennant (triangle) glyph actually\n// renders in the data itself, not only in the static notation key.\nconst pennantIdx = Math.floor(stations.length / 2);\nstations[pennantIdx].u = 34;\nstations[pennantIdx].v = -38;\n\nconst xPad = SPAN_X * 0.12;\nconst yPad = SPAN_Y * 0.14;\nconst xMin = -xPad;\nconst xMax = SPAN_X + xPad;\nconst yMin = -yPad;\nconst yMax = SPAN_Y + yPad;\n\n// --- Wind barb glyph ---------------------------------------------------------\n// Meteorological convention: the staff points toward the direction the wind\n// blows FROM; short barb = 5 kt, full barb = 10 kt, pennant (triangle) = 50 kt,\n// rounded to the nearest 5 kt; calm (< 2.5 kt) draws as a bare open circle.\n// Barbs are attached on a fixed side of the staff (Northern-Hemisphere\n// convention) walking from the tip back toward the station, angled back\n// toward the base like a feather rather than straight perpendicular.\nconst SHAFT_LEN = 44;\nconst BARB_LEN = 15;\nconst BARB_GAP = 8;\nconst CALM_RADIUS = 6;\nconst BARB_ANGLE = (55 * Math.PI) / 180; // degrees off the reverse-staff direction\n\nfunction windBarbShapes(cxPx, cyPx, u, v, color) {\n  const speedRaw = Math.sqrt(u * u + v * v);\n  if (speedRaw < 2.5) {\n    return [{ tag: \"circle\", cx: cxPx, cy: cyPx, r: CALM_RADIUS, color }];\n  }\n\n  // Compass bearing (0 = N, 90 = E) the wind is blowing TOWARD, then flipped\n  // 180° to get the direction the staff points (toward the wind's origin).\n  const toBearing = (Math.atan2(u, v) * 180) / Math.PI;\n  const fromBearing = toBearing + 180;\n  const rad = (fromBearing * Math.PI) / 180;\n  const ux = Math.sin(rad);\n  const uy = -Math.cos(rad);\n  // Fixed perpendicular side for the barbs (consistent across all stations),\n  // then angled back toward the staff's base for an authentic feather look\n  // instead of a straight 90° tick mark.\n  const px = -uy;\n  const py = ux;\n  const bpx = -ux * Math.cos(BARB_ANGLE) + px * Math.sin(BARB_ANGLE);\n  const bpy = -uy * Math.cos(BARB_ANGLE) + py * Math.sin(BARB_ANGLE);\n\n  const tipX = cxPx + ux * SHAFT_LEN;\n  const tipY = cyPx + uy * SHAFT_LEN;\n\n  const shapes = [\n    {\n      tag: \"path\",\n      d: [\n        [\"M\", cxPx, cyPx],\n        [\"L\", tipX, tipY],\n      ],\n      color,\n      fill: \"none\",\n    },\n  ];\n\n  let speed = Math.round(speedRaw / 5) * 5;\n  const pennants = Math.floor(speed / 50);\n  speed -= pennants * 50;\n  const fullBarbs = Math.floor(speed / 10);\n  speed -= fullBarbs * 10;\n  const halfBarb = speed >= 5 ? 1 : 0;\n\n  let pos = SHAFT_LEN;\n  for (let i = 0; i < pennants; i++) {\n    const bx = cxPx + ux * pos;\n    const by = cyPx + uy * pos;\n    const nx = cxPx + ux * (pos - BARB_GAP);\n    const ny = cyPx + uy * (pos - BARB_GAP);\n    const tx = bx + bpx * BARB_LEN;\n    const ty = by + bpy * BARB_LEN;\n    shapes.push({\n      tag: \"path\",\n      d: [[\"M\", bx, by], [\"L\", tx, ty], [\"L\", nx, ny], [\"Z\"]],\n      color,\n      fill: color,\n    });\n    pos -= BARB_GAP;\n  }\n  for (let i = 0; i < fullBarbs; i++) {\n    const bx = cxPx + ux * pos;\n    const by = cyPx + uy * pos;\n    const tx = bx + bpx * BARB_LEN;\n    const ty = by + bpy * BARB_LEN;\n    shapes.push({\n      tag: \"path\",\n      d: [\n        [\"M\", bx, by],\n        [\"L\", tx, ty],\n      ],\n      color,\n      fill: \"none\",\n    });\n    pos -= BARB_GAP;\n  }\n  if (halfBarb) {\n    const bx = cxPx + ux * pos;\n    const by = cyPx + uy * pos;\n    const tx = bx + bpx * (BARB_LEN / 2);\n    const ty = by + bpy * (BARB_LEN / 2);\n    shapes.push({\n      tag: \"path\",\n      d: [\n        [\"M\", bx, by],\n        [\"L\", tx, ty],\n      ],\n      color,\n      fill: \"none\",\n    });\n  }\n\n  return shapes;\n}\n\nfunction paintShape(renderer, group, shape) {\n  if (shape.tag === \"circle\") {\n    renderer\n      .circle(shape.cx, shape.cy, shape.r)\n      .attr({ fill: \"none\", stroke: shape.color, \"stroke-width\": 2 })\n      .add(group);\n  } else {\n    renderer\n      .path(shape.d)\n      .attr({\n        fill: shape.fill === \"none\" ? \"none\" : shape.color,\n        stroke: shape.color,\n        \"stroke-width\": 2,\n        \"stroke-linejoin\": \"round\",\n        \"stroke-linecap\": \"round\",\n      })\n      .add(group);\n  }\n}\n\nfunction drawBarbs(chart) {\n  if (chart.barbGroup) chart.barbGroup.destroy();\n  chart.barbGroup = chart.renderer.g(\"wind-barbs\").add();\n\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  stations.forEach((s) => {\n    const px = xAxis.toPixels(s.x, false);\n    const py = yAxis.toPixels(s.y, false);\n    windBarbShapes(px, py, s.u, s.v, t.palette[0]).forEach((shape) =>\n      paintShape(chart.renderer, chart.barbGroup, shape),\n    );\n  });\n\n  drawLegendKey(chart);\n}\n\n// --- Notation key (barb glyphs are unfamiliar without a legend) ------------\nfunction drawLegendKey(chart) {\n  if (chart.barbLegendGroup) chart.barbLegendGroup.destroy();\n  const renderer = chart.renderer;\n  const group = renderer.g(\"wind-barb-key\").add();\n  chart.barbLegendGroup = group;\n\n  // Lives in the reserved right margin (chart.marginRight below), never over\n  // the plot area, so it can never occlude a station's barb.\n  const boxW = 200;\n  const boxH = 150;\n  const boxX = chart.plotLeft + chart.plotWidth + 16;\n  const boxY = chart.plotTop + 8;\n\n  renderer\n    .rect(boxX, boxY, boxW, boxH, 6)\n    .attr({\n      fill: t.elevatedBg,\n      stroke: t.grid,\n      \"stroke-width\": 1,\n    })\n    .add(group);\n\n  renderer\n    .text(\"Wind speed (kt)\", boxX + 14, boxY + 22)\n    .attr({ zIndex: 5 })\n    .css({ color: t.ink, fontSize: \"13px\", fontWeight: \"600\" })\n    .add(group);\n\n  const entries = [\n    { label: \"Calm (< 2.5)\", u: 0, v: 0 },\n    { label: \"5\", u: 0, v: -5 },\n    { label: \"10\", u: 0, v: -10 },\n    { label: \"25\", u: 0, v: -25 },\n    { label: \"50\", u: 0, v: -50 },\n  ];\n  const rowY0 = boxY + 46;\n  const rowGap = 21;\n  const iconX = boxX + 34;\n  entries.forEach((entry, i) => {\n    const rowY = rowY0 + i * rowGap;\n    windBarbShapes(iconX, rowY, entry.u, entry.v, t.inkSoft).forEach((shape) =>\n      paintShape(renderer, group, shape),\n    );\n    renderer\n      .text(entry.label, boxX + 64, rowY + 5)\n      .css({ color: t.inkSoft, fontSize: \"12px\" })\n      .add(group);\n  });\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    marginRight: 240, // reserves the notation-key panel outside the data area\n    style: { fontFamily: \"inherit\" },\n    events: {\n      render: function () {\n        drawBarbs(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"windbarb-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Surface station network · staff points toward the wind's origin · barbs encode speed\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    min: xMin,\n    max: xMax,\n    startOnTick: false,\n    endOnTick: false,\n    title: {\n      text: \"Longitude (° from reference)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    min: yMin,\n    max: yMax,\n    startOnTick: false,\n    endOnTick: false,\n    title: {\n      text: \"Latitude (° from reference)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false, enableMouseTracking: false },\n    scatter: { marker: { enabled: false } },\n  },\n  series: [\n    {\n      name: \"Stations\",\n      data: stations.map((s) => [s.x, s.y]),\n      marker: { enabled: false },\n    },\n  ],\n});\n"}