{"spec_id":"polar-bar","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// polar-bar: Polar Bar Chart (Wind Rose)\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\n// Only the core `highcharts` bundle is loaded (no highcharts-more), and the\n// PolarComposition that drives `chart.polar` (and thus native polar-column\n// wind roses) lives in highcharts-more.js. As with anyplot's other\n// polar-coordinate Highcharts specs, each stacked wedge is drawn directly as\n// an annular sector with the core SVG renderer — the same `symbols.arc` path\n// primitive Highcharts itself uses internally to draw pie/donut slices — so\n// the chart is real geometry, not a simulation.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (wind rose: % of hourly observations by direction × speed bin) --\nconst DIRECTIONS = [\n  \"N\", \"NNE\", \"NE\", \"ENE\", \"E\", \"ESE\", \"SE\", \"SSE\",\n  \"S\", \"SSW\", \"SW\", \"WSW\", \"W\", \"WNW\", \"NW\", \"NNW\",\n];\nconst SPEED_BINS = [\"0–5 kt\", \"5–10 kt\", \"10–15 kt\", \"15+ kt\"];\n\n// Rows follow DIRECTIONS order; columns follow SPEED_BINS order. A bimodal\n// regime: the dominant onshore southwesterly (rows 9–12) carries both the\n// highest frequency and the highest share of strong gusts, and a lighter\n// secondary northeasterly land breeze (rows 1–3) reverses it part of the day.\nconst FREQUENCY = [\n  [1.3, 1.0, 0.7, 0.4], // N\n  [1.5, 1.4, 1.0, 0.7], // NNE\n  [1.9, 2.2, 1.8, 1.1], // NE\n  [1.6, 1.6, 1.3, 0.9], // ENE\n  [1.3, 1.0, 0.8, 0.5], // E\n  [1.0, 0.8, 0.6, 0.4], // ESE\n  [0.9, 0.7, 0.5, 0.4], // SE\n  [1.1, 0.9, 0.7, 0.4], // SSE\n  [1.5, 1.4, 1.1, 0.8], // S\n  [2.2, 2.8, 2.7, 1.9], // SSW\n  [2.8, 3.8, 3.7, 3.2], // SW\n  [2.6, 3.4, 3.3, 2.8], // WSW\n  [2.3, 2.9, 2.8, 2.5], // W\n  [1.9, 2.1, 2.0, 1.8], // WNW\n  [1.6, 1.5, 1.2, 0.9], // NW\n  [1.4, 1.1, 0.9, 0.6], // NNW\n];\n\nconst SCALE_MAX = 14;\nconst RINGS = [3, 6, 9, 12];\nconst dirCount = DIRECTIONS.length;\n\nconst TITLE = \"polar-bar · javascript · highcharts · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length))) + \"px\";\n\n// --- Chart shell (core-only; the wind rose overlay is drawn below) --------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    margin: [130, 110, 190, 110],\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: titleFontSize, fontWeight: \"600\" },\n  },\n  xAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  yAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink },\n    formatter() {\n      const p = this.point.custom;\n      return `<b>${p.direction}</b><br/>${p.bin}: ${p.value.toFixed(1)}%`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\n// Pin the hidden axes to a known pixel extent so real scatter points can be\n// data-bound at each wedge's centroid — the PNG stays untouched, but the\n// interactive HTML gets genuine tooltips over the rendered geometry.\nchart.xAxis[0].setExtremes(0, chart.plotWidth, false);\nchart.yAxis[0].setExtremes(0, chart.plotHeight, false);\n\n// --- Polar → Cartesian projection ------------------------------------------\nconst centerX = chart.plotLeft + chart.plotWidth / 2;\nconst centerY = chart.plotTop + chart.plotHeight / 2;\nconst radiusMax = Math.min(chart.plotWidth, chart.plotHeight) / 2 - 100;\nconst sectorAngle = (2 * Math.PI) / dirCount;\nconst halfBarWidth = sectorAngle * 0.42;\n\n// Bearing 0 (N) points straight up; bearings proceed clockwise.\nfunction angleFor(dirIndex) {\n  return -Math.PI / 2 + dirIndex * sectorAngle;\n}\nfunction radiusFor(value) {\n  return (radiusMax * value) / SCALE_MAX;\n}\nfunction project(angle, radius) {\n  return [centerX + radius * Math.cos(angle), centerY + radius * Math.sin(angle)];\n}\n\n// --- Concentric grid rings + scale labels -----------------------------------\nRINGS.forEach((level, i) => {\n  chart.renderer\n    .circle(centerX, centerY, radiusFor(level))\n    .attr({\n      fill: \"none\",\n      stroke: t.grid,\n      \"stroke-width\": i === RINGS.length - 1 ? 1.5 : 1,\n      zIndex: 1,\n    })\n    .add();\n});\n\n// Ring labels sit in the gap between N and NNE, clear of every wedge.\nconst ringLabelAngle = angleFor(0) + sectorAngle / 2;\nRINGS.forEach((level) => {\n  const [lx, ly] = project(ringLabelAngle, radiusFor(level));\n  chart.renderer\n    .text(`${level}%`, lx, ly + 5)\n    .attr({ align: \"center\", zIndex: 4 })\n    .css({ fontSize: \"13px\", fontWeight: \"500\", color: t.inkSoft, fontFamily: \"inherit\" })\n    .add();\n});\n\n// --- Direction labels around the perimeter ----------------------------------\nDIRECTIONS.forEach((label, i) => {\n  const angle = angleFor(i);\n  const [lx, ly] = project(angle, radiusMax + 32);\n  const cos = Math.cos(angle);\n  const sin = Math.sin(angle);\n  const align = cos > 0.3 ? \"left\" : cos < -0.3 ? \"right\" : \"center\";\n  chart.renderer\n    .text(label, lx, ly + sin * 6 + 5)\n    .attr({ align, zIndex: 4 })\n    .css({ fontSize: \"14px\", fontWeight: \"600\", color: t.ink, fontFamily: \"inherit\" })\n    .add();\n});\n\n// --- Stacked wedges + real (hidden-marker) scatter points for tooltips -----\nconst tooltipData = [];\n\nDIRECTIONS.forEach((direction, i) => {\n  const startAngle = angleFor(i) - halfBarWidth;\n  const endAngle = angleFor(i) + halfBarWidth;\n  let cumulative = 0;\n\n  SPEED_BINS.forEach((bin, j) => {\n    const value = FREQUENCY[i][j];\n    const innerR = radiusFor(cumulative);\n    const outerR = radiusFor(cumulative + value);\n    cumulative += value;\n\n    const wedgePath = chart.renderer.symbols.arc(centerX, centerY, outerR, outerR, {\n      start: startAngle,\n      end: endAngle,\n      innerR,\n      r: outerR,\n      open: false,\n    });\n    chart.renderer\n      .path(wedgePath)\n      .attr({ fill: t.palette[j], stroke: t.pageBg, \"stroke-width\": 1.5, zIndex: 3 })\n      .add();\n\n    const midAngle = angleFor(i);\n    const midRadius = (innerR + outerR) / 2;\n    const chordWidth = 2 * midRadius * Math.sin(halfBarWidth);\n    const markerRadius = Math.max(8, Math.min(outerR - innerR, chordWidth) / 2);\n    const [px, py] = project(midAngle, midRadius);\n\n    tooltipData.push({\n      x: px - chart.plotLeft,\n      y: chart.plotTop + chart.plotHeight - py,\n      markerRadius,\n      custom: { direction, bin, value },\n    });\n  });\n});\n\nchart.addSeries(\n  {\n    type: \"scatter\",\n    name: \"Frequency\",\n    color: t.palette[0],\n    enableMouseTracking: true,\n    stickyTracking: false,\n    animation: false,\n    marker: {\n      enabled: false,\n      states: { hover: { enabled: true, lineWidth: 2, lineColor: t.pageBg, fillColor: t.ink } },\n    },\n    data: tooltipData.map((d) => ({\n      x: d.x,\n      y: d.y,\n      marker: { radius: d.markerRadius, states: { hover: { radius: d.markerRadius } } },\n      custom: d.custom,\n    })),\n  },\n  false\n);\nchart.redraw();\n\n// --- Legend (speed bins) -----------------------------------------------------\nconst swatchSize = 14;\nconst itemGap = 34;\nconst legendFontSize = 15;\nconst labelWidths = SPEED_BINS.map((label) => label.length * 8.2);\nconst legendTotalWidth = SPEED_BINS.reduce(\n  (sum, _label, i) => sum + swatchSize + 10 + labelWidths[i] + (i < SPEED_BINS.length - 1 ? itemGap : 0),\n  0\n);\nlet cursorX = centerX - legendTotalWidth / 2;\nconst legendY = chart.plotTop + chart.plotHeight + 90;\n\nSPEED_BINS.forEach((label, i) => {\n  chart.renderer\n    .rect(cursorX, legendY - swatchSize / 2, swatchSize, swatchSize, swatchSize / 3)\n    .attr({ fill: t.palette[i], stroke: t.pageBg, \"stroke-width\": 1.5, zIndex: 5 })\n    .add();\n  chart.renderer\n    .text(label, cursorX + swatchSize + 10, legendY + swatchSize / 2 - 2)\n    .attr({ align: \"left\", zIndex: 5 })\n    .css({ fontSize: `${legendFontSize}px`, fontWeight: \"500\", color: t.ink, fontFamily: \"inherit\" })\n    .add();\n  cursorX += swatchSize + 10 + labelWidths[i] + itemGap;\n});\n"}