{"spec_id":"radar-multi","library":"d3","language":"javascript","code":"// anyplot.ai\n// radar-multi: Multi-Series Radar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-20\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Wireless headphone models scored 0-100 across shared review criteria.\nconst axes = [\n  \"Sound Quality\",\n  \"Battery Life\",\n  \"Comfort\",\n  \"Noise Cancel.\",\n  \"Build Quality\",\n  \"Value\",\n];\n\nconst series = [\n  { name: \"Aria Pro\", values: [88, 76, 82, 91, 79, 62] },\n  { name: \"Sonic Wave\", values: [72, 94, 68, 58, 71, 85] },\n  { name: \"Bass Nova\", values: [65, 60, 90, 74, 88, 70] },\n];\n\nconst n = axes.length;\nconst angleSlice = (2 * Math.PI) / n;\nconst maxValue = 100;\nconst levels = [20, 40, 60, 80, 100];\n\n// --- Layout -------------------------------------------------------------\nconst cx = width / 2;\nconst cy = height / 2;\nconst radius = Math.min(width, height) / 2 - 180;\n\nconst radiusScale = d3.scaleLinear().domain([0, maxValue]).range([0, radius]);\nconst color = d3.scaleOrdinal().domain(series.map((s) => s.name)).range(t.palette);\n\n// Emphasize the highest-scoring series as a visual focal point among the\n// overlapping polygons, computed from the data rather than hardcoded.\nconst topSeriesName = series.reduce((top, s) =>\n  d3.mean(s.values) > d3.mean(top.values) ? s : top\n).name;\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// --- Grid: concentric level circles + radial spokes ------------------------\nconst gridGroup = g.append(\"g\");\ngridGroup\n  .selectAll(\"circle\")\n  .data(levels)\n  .join(\"circle\")\n  .attr(\"r\", (d) => radiusScale(d))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\nconst axisAngle = (i) => i * angleSlice - Math.PI / 2;\n\n// Place gridline value labels in the gap between the last and first axis\n// spokes, well clear of any axis's data markers regardless of data values.\nconst levelLabelAngle = axisAngle(0) - angleSlice / 2;\nconst levelLabelCos = Math.cos(levelLabelAngle);\nconst levelLabelSin = Math.sin(levelLabelAngle);\n\ngridGroup\n  .selectAll(\".level-label\")\n  .data(levels)\n  .join(\"text\")\n  .attr(\"class\", \"level-label\")\n  .attr(\"x\", (d) => (radiusScale(d) + 6) * levelLabelCos)\n  .attr(\"y\", (d) => (radiusScale(d) + 6) * levelLabelSin)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text((d) => d);\n\ng.append(\"text\")\n  .attr(\"class\", \"level-caption\")\n  .attr(\"x\", (radius + 20) * levelLabelCos)\n  .attr(\"y\", (radius + 20) * levelLabelSin)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"11px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Score (0–100)\");\n\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(axes)\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"y1\", 0)\n  .attr(\"x2\", (d, i) => radiusScale(maxValue) * Math.cos(axisAngle(i)))\n  .attr(\"y2\", (d, i) => radiusScale(maxValue) * Math.sin(axisAngle(i)))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axis labels --------------------------------------------------------\nconst labelRadius = radius + 34;\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(axes)\n  .join(\"text\")\n  .attr(\"x\", (d, i) => labelRadius * Math.cos(axisAngle(i)))\n  .attr(\"y\", (d, i) => labelRadius * Math.sin(axisAngle(i)))\n  .attr(\"text-anchor\", (d, i) => {\n    const c = Math.cos(axisAngle(i));\n    if (c > 0.15) return \"start\";\n    if (c < -0.15) return \"end\";\n    return \"middle\";\n  })\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"500\")\n  .text((d) => d);\n\n// --- Data polygons --------------------------------------------------------\nconst radarLine = d3\n  .lineRadial()\n  .angle((d, i) => i * angleSlice)\n  .radius((d) => radiusScale(d))\n  .curve(d3.curveLinearClosed);\n\nconst seriesGroup = g.append(\"g\");\n\nseriesGroup\n  .selectAll(\".radar-area\")\n  .data(series)\n  .join(\"path\")\n  .attr(\"class\", \"radar-area\")\n  .attr(\"d\", (d) => radarLine(d.values))\n  .attr(\"fill\", (d) => color(d.name))\n  .attr(\"fill-opacity\", (d) => (d.name === topSeriesName ? 0.3 : 0.18))\n  .attr(\"stroke\", (d) => color(d.name))\n  .attr(\"stroke-width\", (d) => (d.name === topSeriesName ? 4 : 2.5))\n  .attr(\"stroke-linejoin\", \"round\");\n\nseriesGroup\n  .selectAll(\".radar-dots\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"radar-dots\")\n  .attr(\"fill\", (d) => color(d.name))\n  .each(function (d) {\n    d3.select(this)\n      .selectAll(\"circle\")\n      .data(d.values)\n      .join(\"circle\")\n      .attr(\"cx\", (v, i) => radiusScale(v) * Math.cos(axisAngle(i)))\n      .attr(\"cy\", (v, i) => radiusScale(v) * Math.sin(axisAngle(i)))\n      .attr(\"r\", d.name === topSeriesName ? 7 : 5.5)\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 1.5);\n  });\n\n// --- Legend -----------------------------------------------------------------\nconst legendItemWidth = 190;\nconst legendWidth = series.length * legendItemWidth;\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${cx - legendWidth / 2},${height - 56})`);\n\nconst legendItems = legend\n  .selectAll(\".legend-item\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(${i * legendItemWidth},0)`);\n\nlegendItems\n  .append(\"rect\")\n  .attr(\"width\", 18)\n  .attr(\"height\", 18)\n  .attr(\"y\", -14)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => color(d.name));\n\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", 0)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.name);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"radar-multi · javascript · d3 · anyplot.ai\");\n"}