{"spec_id":"windrose-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// windrose-basic: Wind Rose Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// One year of hourly wind observations from a coastal weather station,\n// binned into 8 compass sectors × 4 speed classes. Values are % of all\n// observations and sum to 100 (calm-sector totals show the prevailing\n// westerly/south-westerly wind with a secondary north-westerly lobe from\n// post-frontal outflow).\nconst directions = [\"N\", \"NE\", \"E\", \"SE\", \"S\", \"SW\", \"W\", \"NW\"];\nconst speedBinLabels = [\"0–5 m/s\", \"5–10 m/s\", \"10–15 m/s\", \"15+ m/s\"];\n\nconst frequencyCalm = [2.5, 2.2, 2.2, 2.8, 3.5, 5.0, 5.0, 4.0];\nconst frequencyLight = [2.0, 1.8, 1.8, 2.5, 3.8, 8.0, 8.5, 6.0];\nconst frequencyModerate = [1.0, 0.7, 0.7, 1.2, 2.5, 7.0, 7.5, 4.5];\nconst frequencyStrong = [0.5, 0.3, 0.3, 0.5, 1.2, 4.0, 4.0, 2.5];\nconst frequencyBySpeedBin = [\n  frequencyCalm,\n  frequencyLight,\n  frequencyModerate,\n  frequencyStrong,\n];\n\n// Chart.js polarArea draws dataset 0 last (on top). Feeding it the running\n// cumulative sum per bin — smallest (calm) first, total last — turns the\n// overlapping wedges into an honest stacked ring per direction: each dataset's\n// arc reaches exactly its own cumulative frequency, so the visible band width\n// between two rings equals that bin's real share, not an approximation.\nconst cumulativeBySpeedBin = frequencyBySpeedBin.map((_, bin) =>\n  directions.map((_, dir) =>\n    frequencyBySpeedBin\n      .slice(0, bin + 1)\n      .reduce((sum, series) => sum + series[dir], 0),\n  ),\n);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"polarArea\",\n  data: {\n    labels: directions,\n    datasets: frequencyBySpeedBin.map((_, bin) => ({\n      data: cumulativeBySpeedBin[bin],\n      backgroundColor: t.palette[bin],\n      borderColor: t.pageBg,\n      borderWidth: 2,\n    })),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"windrose-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n      },\n      subtitle: {\n        display: true,\n        text: \"Frequency of observations (%)\",\n        color: t.inkSoft,\n        font: { size: 15 },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          boxWidth: 20,\n          boxHeight: 14,\n          // Default polarArea legend lists one entry per direction, all colored\n          // from dataset 0 — not useful here. Build one entry per speed bin instead.\n          generateLabels: (chart) =>\n            speedBinLabels.map((label, bin) => ({\n              text: label,\n              fillStyle: t.palette[bin],\n              strokeStyle: t.palette[bin],\n              fontColor: t.inkSoft,\n              index: bin,\n              hidden: !chart.isDatasetVisible(bin),\n            })),\n        },\n        onClick: (evt, item, legend) => {\n          const chart = legend.chart;\n          chart.setDatasetVisibility(\n            item.index,\n            !chart.isDatasetVisible(item.index),\n          );\n          chart.update();\n        },\n      },\n    },\n    scales: {\n      r: {\n        beginAtZero: true,\n        angleLines: { color: t.grid },\n        grid: { color: t.grid, circular: true },\n        pointLabels: {\n          display: true,\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n        },\n        ticks: {\n          color: t.ink,\n          // z > 0 draws tick labels after the datasets (see Chart.js core.scale\n          // _layers()) — without it the opaque wedges paint over the numbers\n          // wherever a wedge's radius exceeds a gridline.\n          z: 1,\n          showLabelBackdrop: true,\n          backdropColor: t.elevatedBg,\n          backdropPadding: 4,\n          font: { size: 13 },\n          callback: (value) => value + \"%\",\n        },\n      },\n    },\n  },\n});\n"}