{"spec_id":"windbarb-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// windbarb-basic: Wind Barb Plot for Meteorological Data\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: surface wind observations around a developing low-pressure system\n// Station grid spacing (km), low center offset off-grid so no station sits\n// exactly at the vortex core.\nconst xsKm = [0, 50, 100, 150, 200, 250, 300, 350, 400];\nconst ysKm = [0, 50, 100, 150, 200];\nconst centerX = 190;\nconst centerY = 115;\n\nconst stations = [];\nfor (const x of xsKm) {\n  for (const y of ysKm) {\n    const dx = x - centerX;\n    const dy = y - centerY;\n    const r = Math.sqrt(dx * dx + dy * dy);\n    // Counterclockwise tangential flow plus inward spiral (cyclonic pattern).\n    const tangentX = -dy / r;\n    const tangentY = dx / r;\n    const inwardX = -dx / r;\n    const inwardY = -dy / r;\n    const dirX = 0.8 * tangentX + 0.5 * inwardX;\n    const dirY = 0.8 * tangentY + 0.5 * inwardY;\n    const dirNorm = Math.hypot(dirX, dirY);\n    const speedKt = 90 * Math.exp(-r / 65);\n    const u = (dirX / dirNorm) * speedKt;\n    const v = (dirY / dirNorm) * speedKt;\n    stations.push([x, y, u, v]);\n  }\n}\n\n// --- Wind barb rendering (staff + half/full barbs + pennants) ---------------\nconst STAFF_LEN = 42;\nconst BARB_GAP = 8;\nconst FULL_SPAN = 15;\nconst HALF_SPAN = 8;\nconst CALM_KT = 2.5;\n\nfunction speedToCounts(speedKt) {\n  const rounded = Math.round(speedKt / 5) * 5;\n  const pennants = Math.floor(rounded / 50);\n  let remainder = rounded - pennants * 50;\n  const fulls = Math.floor(remainder / 10);\n  remainder -= fulls * 10;\n  const half = remainder >= 5;\n  return { pennants, fulls, half };\n}\n\nfunction renderBarb(params, api) {\n  const x = api.value(0);\n  const y = api.value(1);\n  const u = api.value(2);\n  const v = api.value(3);\n  const station = api.coord([x, y]);\n  const speedKt = Math.hypot(u, v);\n\n  if (speedKt < CALM_KT) {\n    return {\n      type: \"circle\",\n      shape: { cx: station[0], cy: station[1], r: 5 },\n      style: api.style({ fill: \"none\", stroke: t.palette[0], lineWidth: 2 }),\n    };\n  }\n\n  // Staff points FROM which the wind blows — opposite the (u, v) vector.\n  const tip = api.coord([x - u, y - v]);\n  let dirX = tip[0] - station[0];\n  let dirY = tip[1] - station[1];\n  const dirLen = Math.hypot(dirX, dirY);\n  dirX /= dirLen;\n  dirY /= dirLen;\n  const perpX = -dirY;\n  const perpY = dirX;\n\n  function toPixel(along, across) {\n    return [\n      station[0] + dirX * along + perpX * across,\n      station[1] + dirY * along + perpY * across,\n    ];\n  }\n\n  const { pennants, fulls, half } = speedToCounts(speedKt);\n  const lineStyle = api.style({ stroke: t.palette[0], fill: \"none\", lineWidth: 2.5 });\n  const fillStyle = api.style({ fill: t.palette[0], stroke: t.palette[0] });\n  const children = [\n    { type: \"polyline\", shape: { points: [[station[0], station[1]], toPixel(STAFF_LEN, 0)] }, style: lineStyle },\n  ];\n\n  let pos = STAFF_LEN;\n  for (let i = 0; i < pennants; i++) {\n    children.push({\n      type: \"polygon\",\n      shape: { points: [toPixel(pos, 0), toPixel(pos - BARB_GAP, 0), toPixel(pos - BARB_GAP / 2, -FULL_SPAN)] },\n      style: fillStyle,\n    });\n    pos -= BARB_GAP;\n  }\n  for (let i = 0; i < fulls; i++) {\n    children.push({\n      type: \"polyline\",\n      shape: { points: [toPixel(pos, 0), toPixel(pos - FULL_SPAN * 0.35, -FULL_SPAN)] },\n      style: lineStyle,\n    });\n    pos -= BARB_GAP;\n  }\n  if (half) {\n    children.push({\n      type: \"polyline\",\n      shape: { points: [toPixel(pos, 0), toPixel(pos - HALF_SPAN * 0.35, -HALF_SPAN)] },\n      style: lineStyle,\n    });\n  }\n\n  return { type: \"group\", children };\n}\n\n// --- Chart --------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"Cyclonic Wind Field · windbarb-basic · javascript · echarts · anyplot.ai\",\n    subtext: \"Barb notation: half barb = 5 kt · full barb = 10 kt · pennant = 50 kt · open circle = calm\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 20 },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: { left: 100, right: 70, top: 150, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    name: \"Distance east of station network origin (km)\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    min: -30,\n    max: 430,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Distance north of station network origin (km)\",\n    nameLocation: \"middle\",\n    nameGap: 55,\n    min: -20,\n    max: 220,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      encode: { x: 0, y: 1 },\n      renderItem: renderBarb,\n      data: stations,\n    },\n  ],\n});\n"}