{"spec_id":"windbarb-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// windbarb-basic: Wind Barb Plot for Meteorological Data\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG) ------------------------------------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\n// --- Data: surface wind observations around a synthetic low-pressure center -\n// A Rankine combined vortex — solid-body rotation inside the core radius,\n// irrotational decay outside — gives a realistic cyclonic wind field: calm at\n// the exact center, peak speed at the core edge, and tapering outward.\nconst NX = 9;\nconst NY = 7;\nconst CENTER_I = 4;\nconst CENTER_J = 3;\nconst CORE_RADIUS = 1.8; // grid units\nconst PEAK_SPEED = 55; // knots, tangential speed at the core radius\n\nconst stations = [];\nfor (let j = 0; j < NY; j++) {\n  for (let i = 0; i < NX; i++) {\n    const dx = i - CENTER_I;\n    const dy = j - CENTER_J;\n    const r = Math.sqrt(dx * dx + dy * dy);\n    const vTan = r <= CORE_RADIUS ? PEAK_SPEED * (r / CORE_RADIUS) : PEAK_SPEED * (CORE_RADIUS / r);\n    const vRad = -0.12 * vTan; // mild inflow toward the low\n    let u = 0; // eastward component (knots)\n    let v = 0; // northward component (knots)\n    if (r > 1e-9) {\n      u = vTan * (-dy / r) + vRad * (dx / r);\n      v = vTan * (dx / r) + vRad * (dy / r);\n      u += (lcg() - 0.5) * 3;\n      v += (lcg() - 0.5) * 3;\n    }\n    stations.push({ lon: 10 + i, lat: 45 + j, u, v });\n  }\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 110, right: 300, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst xScale = d3.scaleLinear().domain([9.5, 18.5]).range([0, iw]);\nconst yScale = d3.scaleLinear().domain([44.5, 51.5]).range([ih, 0]);\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(xScale)\n      .tickValues(d3.range(10, 19, 2))\n      .tickFormat((d) => `${d}°E`)\n  );\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(yScale)\n    .tickValues(d3.range(45, 52, 1))\n    .tickFormat((d) => `${d}°N`)\n);\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Longitude\");\n\ng.append(\"text\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -68)\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Latitude\");\n\n// --- Wind barb glyph ---------------------------------------------------------\n// Meteorological convention: the staff points toward the direction FROM which\n// the wind blows; barbs/pennants sit on the left side of the staff (Northern\n// Hemisphere) and are read outer-tip-inward as 50 kt (pennant), 10 kt (full\n// barb), 5 kt (half barb). Calm winds (< 2.5 kt) draw as an open circle.\nconst CALM_KNOTS = 2.5;\nconst BARB_ANGLE = (55 * Math.PI) / 180; // tilt of each barb off the staff\n\nfunction barbTip(len) {\n  return [-len * Math.sin(BARB_ANGLE), -len * Math.cos(BARB_ANGLE)];\n}\n\n// Draws one barb (dot + staff + speed notation) into `parent`, already\n// translated to the station's screen position. `u`/`v` are eastward/northward\n// wind components in knots; sizes scale the staff/barb/pennant geometry so the\n// same routine renders both the map glyphs and the compact legend key.\nfunction drawBarb(parent, u, v, sizes) {\n  const speed = Math.sqrt(u * u + v * v);\n\n  if (speed < CALM_KNOTS) {\n    parent.append(\"circle\").attr(\"r\", sizes.calmR).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 2.5);\n    return;\n  }\n\n  const theta = (Math.atan2(-u, -v) * 180) / Math.PI;\n  const rounded = Math.round(speed / 5) * 5;\n  let remaining = rounded;\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\n  const rotated = parent.append(\"g\").attr(\"transform\", `rotate(${theta})`);\n\n  rotated.append(\"circle\").attr(\"r\", sizes.dotR).attr(\"fill\", t.palette[0]);\n  rotated\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"y1\", 0)\n    .attr(\"x2\", 0)\n    .attr(\"y2\", -sizes.staffLen)\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 2.5)\n    .attr(\"stroke-linecap\", \"round\");\n\n  let y = -sizes.staffLen;\n  for (let k = 0; k < pennants; k++) {\n    const yTip = y;\n    const yBase = y + sizes.step;\n    const [ox, oy] = barbTip(sizes.pennantLen);\n    const apex = [ox, (yTip + yBase) / 2 + oy];\n    rotated.append(\"path\").attr(\"d\", `M0,${yTip} L0,${yBase} L${apex[0]},${apex[1]} Z`).attr(\"fill\", t.palette[0]);\n    y += sizes.step + sizes.gap;\n  }\n  for (let k = 0; k < fullBarbs; k++) {\n    const [ox, oy] = barbTip(sizes.barbLen);\n    rotated\n      .append(\"line\")\n      .attr(\"x1\", 0)\n      .attr(\"y1\", y)\n      .attr(\"x2\", ox)\n      .attr(\"y2\", y + oy)\n      .attr(\"stroke\", t.palette[0])\n      .attr(\"stroke-width\", 2.5)\n      .attr(\"stroke-linecap\", \"round\");\n    y += sizes.step + sizes.gap;\n  }\n  if (halfBarb) {\n    const [ox, oy] = barbTip(sizes.halfLen);\n    rotated\n      .append(\"line\")\n      .attr(\"x1\", 0)\n      .attr(\"y1\", y)\n      .attr(\"x2\", ox)\n      .attr(\"y2\", y + oy)\n      .attr(\"stroke\", t.palette[0])\n      .attr(\"stroke-width\", 2.5)\n      .attr(\"stroke-linecap\", \"round\");\n  }\n}\n\nconst MAP_SIZES = { staffLen: 68, barbLen: 22, halfLen: 11, pennantLen: 22, step: 9, gap: 3, dotR: 3.5, calmR: 9 };\n\nconst stationLayer = g.append(\"g\");\nfor (const s of stations) {\n  const stationGroup = stationLayer.append(\"g\").attr(\"transform\", `translate(${xScale(s.lon)},${yScale(s.lat)})`);\n  drawBarb(stationGroup, s.u, s.v, MAP_SIZES);\n}\n\n// --- Legend: symbol key -------------------------------------------------\n// A north-pointing staff (u=0, v=-knots) rotates to theta=0, so the same\n// drawBarb routine used on the map also renders clean, upright legend glyphs.\nconst LEGEND_SIZES = { staffLen: 46, barbLen: 18, halfLen: 9, pennantLen: 18, step: 8, gap: 3, dotR: 3, calmR: 7 };\nconst legendEntries = [\n  { knots: 0, label: \"Calm (< 2.5 kt)\" },\n  { knots: 5, label: \"Half barb — 5 kt\" },\n  { knots: 10, label: \"Full barb — 10 kt\" },\n  { knots: 50, label: \"Pennant — 50 kt\" },\n];\nconst legendX = margin.left + iw + 70;\nconst legendY = margin.top + 40;\nconst legendRowH = 62;\nconst legendBoxW = 220;\nconst legendBoxH = legendEntries.length * legendRowH + 46;\n\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${legendX},${legendY})`);\nlegend\n  .append(\"rect\")\n  .attr(\"x\", -30)\n  .attr(\"y\", -34)\n  .attr(\"width\", legendBoxW)\n  .attr(\"height\", legendBoxH)\n  .attr(\"rx\", 10)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid);\nlegend.append(\"text\").attr(\"x\", 20).attr(\"y\", -6).attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"15px\").style(\"font-weight\", \"600\").text(\"Wind speed key\");\n\nlegendEntries.forEach((entry, idx) => {\n  const rowY = idx * legendRowH + 28;\n  const glyph = legend.append(\"g\").attr(\"transform\", `translate(20,${rowY})`);\n  drawBarb(glyph, 0, entry.knots === 0 ? 0 : -entry.knots, LEGEND_SIZES);\n  legend\n    .append(\"text\")\n    .attr(\"x\", 56)\n    .attr(\"y\", rowY - LEGEND_SIZES.staffLen / 2 + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(entry.label);\n});\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"windbarb-basic · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 84)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Surface wind observations around a low-pressure system\");\n"}