{"spec_id":"polar-line","library":"d3","language":"javascript","code":"// anyplot.ai\n// polar-line: Polar Line Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Average wind speed (km/h) by compass direction at a coastal weather station,\n// summer vs. winter — prevailing westerlies, stronger in winter.\nconst directions = [\n  \"N\", \"NNE\", \"NE\", \"ENE\", \"E\", \"ESE\", \"SE\", \"SSE\",\n  \"S\", \"SSW\", \"SW\", \"WSW\", \"W\", \"WNW\", \"NW\", \"NNW\",\n];\nconst summerSpeed = [12, 10, 9, 8, 7, 6, 7, 9, 12, 15, 18, 22, 25, 23, 19, 15];\nconst winterSpeed = [18, 16, 14, 12, 10, 9, 10, 13, 17, 22, 27, 32, 36, 33, 28, 22];\n\n// Winter intentionally uses palette[2] (blue) rather than the canonical\n// palette[1] (lavender) — a deliberate warm/cold semantic pairing with the\n// brand-green Summer series, not an arbitrary skip.\nconst series = [\n  { name: \"Summer\", values: summerSpeed, color: t.palette[0] },\n  { name: \"Winter\", values: winterSpeed, color: t.palette[2] },\n];\n\n// --- Layout -------------------------------------------------------------------\nconst centerX = width / 2;\nconst centerY = 620;\nconst outerRadius = 420;\nconst labelOffset = 34;\nconst maxSpeed = Math.max(...summerSpeed, ...winterSpeed);\n\nconst angle = d3.scaleLinear().domain([0, directions.length]).range([0, 2 * Math.PI]);\nconst radius = d3.scaleLinear().domain([0, maxSpeed]).nice().range([0, outerRadius]);\nconst radiusTicks = radius.ticks(4).filter((v) => v > 0);\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(${centerX},${centerY})`);\n\n// --- Grid: concentric circles + radial spokes ------------------------------\nconst gridGroup = g.append(\"g\");\ngridGroup\n  .selectAll(\"circle\")\n  .data(radiusTicks)\n  .join(\"circle\")\n  .attr(\"r\", (d) => radius(d))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ngridGroup\n  .selectAll(\"line\")\n  .data(d3.range(directions.length))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"y1\", 0)\n  .attr(\"x2\", (i) => outerRadius * Math.sin(angle(i)))\n  .attr(\"y2\", (i) => -outerRadius * Math.cos(angle(i)))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Radial tick labels (placed along a spoke clear of the data lines) ------\nconst tickAngle = angle(0.5); // halfway between N and NNE, away from the peak lobe\ng.selectAll(\".radius-tick\")\n  .data(radiusTicks)\n  .join(\"text\")\n  .attr(\"class\", \"radius-tick\")\n  .attr(\"x\", (d) => radius(d) * Math.sin(tickAngle) + 6)\n  .attr(\"y\", (d) => -radius(d) * Math.cos(tickAngle))\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => `${d} km/h`);\n\n// --- Direction labels ---------------------------------------------------------\ng.selectAll(\".direction-label\")\n  .data(d3.range(directions.length))\n  .join(\"text\")\n  .attr(\"class\", \"direction-label\")\n  .attr(\"x\", (i) => (outerRadius + labelOffset) * Math.sin(angle(i)))\n  .attr(\"y\", (i) => -(outerRadius + labelOffset) * Math.cos(angle(i)))\n  .attr(\"text-anchor\", (i) => {\n    const s = Math.sin(angle(i));\n    if (s > 0.15) return \"start\";\n    if (s < -0.15) return \"end\";\n    return \"middle\";\n  })\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((i) => directions[i]);\n\n// --- Radial fill between the two series — spotlights the seasonal gap -------\nconst seasonGap = d3\n  .areaRadial()\n  .angle((d, i) => angle(i))\n  .innerRadius((d, i) => radius(summerSpeed[i]))\n  .outerRadius((d, i) => radius(winterSpeed[i]))\n  .curve(d3.curveCardinalClosed);\n\ng.append(\"path\")\n  .datum(d3.range(directions.length))\n  .attr(\"d\", seasonGap)\n  .attr(\"fill\", series[1].color)\n  .attr(\"fill-opacity\", 0.12)\n  .attr(\"stroke\", \"none\");\n\n// --- Lines (closed loop — cyclical data) + markers ---------------------------\nconst lineRadial = d3\n  .lineRadial()\n  .angle((d, i) => angle(i))\n  .radius((d) => radius(d))\n  .curve(d3.curveCardinalClosed);\n\nfor (const s of series) {\n  g.append(\"path\")\n    .datum(s.values)\n    .attr(\"d\", lineRadial)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", 3)\n    .attr(\"stroke-linejoin\", \"round\");\n\n  g.selectAll(`.marker-${s.name}`)\n    .data(s.values)\n    .join(\"circle\")\n    .attr(\"class\", `marker-${s.name}`)\n    .attr(\"cx\", (d, i) => radius(d) * Math.sin(angle(i)))\n    .attr(\"cy\", (d, i) => -radius(d) * Math.cos(angle(i)))\n    .attr(\"r\", 5)\n    .attr(\"fill\", s.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n}\n\n// --- Legend -------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${centerX - 110},${height - 46})`);\nseries.forEach((s, i) => {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${i * 130},0)`);\n  item\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 24)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", 3);\n  item\n    .append(\"circle\")\n    .attr(\"cx\", 12)\n    .attr(\"cy\", 0)\n    .attr(\"r\", 5)\n    .attr(\"fill\", s.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n  item\n    .append(\"text\")\n    .attr(\"x\", 32)\n    .attr(\"y\", 0)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(s.name);\n});\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 55)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"30px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"polar-line · javascript · d3 · anyplot.ai\");\n"}