{"spec_id":"windrose-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// windrose-basic: Wind Rose Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 96/100 | Created: 2026-08-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data --------------------------------------------------------------\n// Hourly wind observations from an offshore wind-farm site-assessment study\n// (one year), aggregated into 8 compass sectors x 4 speed bins. Values are\n// percent of all observations; direction totals show a prevailing SW/W\n// pattern typical of Northern Hemisphere mid-latitude sites.\nconst directions = [\"N\", \"NE\", \"E\", \"SE\", \"S\", \"SW\", \"W\", \"NW\"];\nconst speedBins = [\"0-5 m/s\", \"5-10 m/s\", \"10-15 m/s\", \"15+ m/s\"];\nconst frequency = [\n  [3.93, 2.14, 0.86, 0.21], // N\n  [3.43, 1.6, 0.57, 0.11], // NE\n  [2.79, 1.07, 0.34, 0.09], // E\n  [3.31, 1.71, 0.57, 0.11], // SE\n  [4.5, 3.2, 1.8, 0.5], // S\n  [6.0, 7.5, 5.79, 2.14], // SW\n  [6.29, 9.71, 8.57, 4.0], // W\n  [6.0, 5.83, 3.94, 1.37], // NW\n];\n\nconst stackKeys = d3.range(speedBins.length);\nconst stackRows = directions.map((dir, i) => {\n  const row = { dir, i };\n  frequency[i].forEach((value, b) => (row[b] = value));\n  return row;\n});\nconst segments = d3\n  .stack()\n  .keys(stackKeys)(stackRows)\n  .flatMap((series, b) => series.map((d) => ({ dir: d.data.dir, i: d.data.i, b, seg: [d[0], d[1]] })));\n\n// Dominant direction (highest total frequency) drives the storytelling highlight below.\nconst dominant = d3.greatest(\n  directions.map((dir, i) => ({ dir, i, total: d3.sum(frequency[i]) })),\n  (d) => d.total,\n);\n\n// --- Scales --------------------------------------------------------------\nconst cx = width / 2;\nconst cy = height / 2 + 20;\nconst outerR = 380;\nconst innerR = 40;\nconst gridMax = 30;\nconst gridTicks = [10, 20, 30];\n\nconst radius = d3.scaleLinear().domain([0, gridMax]).range([innerR, outerR]);\nconst angleStep = (2 * Math.PI) / directions.length;\nconst padAngle = 0.05;\nconst color = d3.scaleOrdinal().domain(speedBins).range(t.palette.slice(0, speedBins.length));\n\n// --- SVG mount -------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${cx},${cy})`);\n\n// --- Dominant-sector highlight (behind grid, drives the storytelling) -------\nconst highlightArc = d3\n  .arc()\n  .innerRadius(innerR - 12)\n  .outerRadius(outerR + 24)\n  .cornerRadius(8)\n  .startAngle(dominant.i * angleStep - angleStep / 2)\n  .endAngle(dominant.i * angleStep + angleStep / 2);\n\ng.append(\"path\").attr(\"d\", highlightArc).attr(\"fill\", t.ink).attr(\"opacity\", 0.06);\n\n// --- Radial grid circles + percentage labels --------------------------------\nconst gridGroup = g.append(\"g\");\ngridGroup\n  .selectAll(\"circle\")\n  .data(gridTicks)\n  .join(\"circle\")\n  .attr(\"r\", (d) => radius(d))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5);\n\nconst tickAngle = angleStep / 2;\ngridGroup\n  .selectAll(\"text\")\n  .data(gridTicks)\n  .join(\"text\")\n  .attr(\"x\", (d) => radius(d) * Math.sin(tickAngle) + 8)\n  .attr(\"y\", (d) => -radius(d) * Math.cos(tickAngle))\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => `${d}%`);\n\n// --- Angular spokes ----------------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(directions)\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"y1\", 0)\n  .attr(\"x2\", (_, i) => outerR * Math.sin(i * angleStep))\n  .attr(\"y2\", (_, i) => -outerR * Math.cos(i * angleStep))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Stacked direction sectors ------------------------------------------------\nconst arcGen = d3\n  .arc()\n  .innerRadius((d) => radius(d.seg[0]))\n  .outerRadius((d) => radius(d.seg[1]))\n  .startAngle((d) => d.i * angleStep - angleStep / 2 + padAngle / 2)\n  .endAngle((d) => d.i * angleStep + angleStep / 2 - padAngle / 2);\n\ng.append(\"g\")\n  .selectAll(\"path\")\n  .data(segments)\n  .join(\"path\")\n  .attr(\"d\", arcGen)\n  .attr(\"fill\", (d) => color(speedBins[d.b]))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Compass direction labels --------------------------------------------------\nconst labelR = outerR + 42;\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(directions)\n  .join(\"text\")\n  .attr(\"x\", (_, i) => labelR * Math.sin(i * angleStep))\n  .attr(\"y\", (_, i) => -labelR * Math.cos(i * angleStep))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d);\n\n// --- Title + storytelling subtitle -------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 58)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"32px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"windrose-basic · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 92)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text(`Prevailing winds from the ${dominant.dir} sector — ${dominant.total.toFixed(1)}% of observations`);\n\n// --- Legend (speed-bin swatches) --------------------------------------------\nconst legendY = height - 56;\nconst slotWidth = 250;\nconst legendStartX = width / 2 - (slotWidth * speedBins.length) / 2;\n\nconst legend = svg.append(\"g\");\nlegend\n  .selectAll(\"rect\")\n  .data(speedBins)\n  .join(\"rect\")\n  .attr(\"x\", (_, i) => legendStartX + i * slotWidth)\n  .attr(\"y\", legendY - 13)\n  .attr(\"width\", 22)\n  .attr(\"height\", 22)\n  .attr(\"fill\", (d) => color(d));\n\nlegend\n  .selectAll(\"text\")\n  .data(speedBins)\n  .join(\"text\")\n  .attr(\"x\", (_, i) => legendStartX + i * slotWidth + 30)\n  .attr(\"y\", legendY + 4)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text((d) => d);\n"}