{"spec_id":"radar-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// radar-basic: Basic Radar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 95/100 | Created: 2026-07-24\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\nconst categories = [\n  \"Communication\", \"Technical Skills\", \"Teamwork\",\n  \"Leadership\", \"Problem Solving\", \"Adaptability\",\n];\nconst series = [\n  { name: \"Software Engineer\", values: [85, 92, 78, 65, 88, 74], color: t.palette[0] },\n  { name: \"Team Average\", values: [70, 75, 72, 68, 70, 71], color: t.palette[1] },\n];\nconst maxValue = 100;\nconst gridLevels = [20, 40, 60, 80, 100];\n\n// Largest cross-series gap — drives the storytelling highlight below.\nconst gaps = categories.map((_, i) => {\n  const vals = series.map((s) => s.values[i]);\n  return Math.max(...vals) - Math.min(...vals);\n});\nconst maxGapIndex = gaps.indexOf(Math.max(...gaps));\nconst gapVals = series.map((s) => s.values[maxGapIndex]);\nconst gapHigh = Math.max(...gapVals);\nconst gapLow = Math.min(...gapVals);\n\n// --- Layout ------------------------------------------------------------------\nconst titleSpace = 90;\nconst legendSpace = 70;\nconst cx = width / 2;\nconst cy = titleSpace + (height - titleSpace - legendSpace) / 2;\nconst radius = Math.min(width, height - titleSpace - legendSpace) / 2 - 90;\nconst angleSlice = (2 * Math.PI) / categories.length;\nconst rScale = d3.scaleLinear().domain([0, maxValue]).range([0, radius]);\n\nconst angleFor = (i) => i * angleSlice - Math.PI / 2;\nconst point = (i, value) => {\n  const a = angleFor(i);\n  return [cx + rScale(value) * Math.cos(a), cy + rScale(value) * Math.sin(a)];\n};\nconst anchorFor = (i) => {\n  const cos = Math.cos(angleFor(i));\n  return cos > 0.15 ? \"start\" : cos < -0.15 ? \"end\" : \"middle\";\n};\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Storytelling highlight: shade the sector with the largest series gap ----\nconst gapArc = d3.arc()\n  .innerRadius(0)\n  .outerRadius(radius)\n  .startAngle((maxGapIndex - 0.5) * angleSlice)\n  .endAngle((maxGapIndex + 0.5) * angleSlice);\n\nsvg.append(\"path\")\n  .attr(\"transform\", `translate(${cx},${cy})`)\n  .attr(\"d\", gapArc())\n  .attr(\"fill\", t.amber)\n  .attr(\"fill-opacity\", 0.08);\n\n// --- Grid: concentric webs with visual hierarchy (outer ring emphasized) -----\nconst ringOpacity = d3.scaleLinear().domain([d3.min(gridLevels), d3.max(gridLevels)]).range([0.35, 0.85]);\n\nsvg.selectAll(\".grid-ring\")\n  .data(gridLevels)\n  .join(\"polygon\")\n  .attr(\"class\", \"grid-ring\")\n  .attr(\"points\", (level) => categories.map((_, i) => point(i, level).join(\",\")).join(\" \"))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", (level) => (level === maxValue ? 1.75 : 1))\n  .attr(\"stroke-opacity\", ringOpacity);\n\n// --- Spokes (largest-gap axis emphasized in amber) ----------------------------\nsvg.selectAll(\".spoke\")\n  .data(categories)\n  .join(\"line\")\n  .attr(\"class\", \"spoke\")\n  .attr(\"x1\", cx).attr(\"y1\", cy)\n  .attr(\"x2\", (_, i) => point(i, maxValue)[0])\n  .attr(\"y2\", (_, i) => point(i, maxValue)[1])\n  .attr(\"stroke\", (_, i) => (i === maxGapIndex ? t.amber : t.grid))\n  .attr(\"stroke-width\", (_, i) => (i === maxGapIndex ? 1.75 : 1));\n\n// --- Radial scale reference on two opposite spokes (top + bottom) ------------\nconst referenceAxes = [\n  { index: 0, opacity: 1 },\n  { index: Math.floor(categories.length / 2), opacity: 0.55 },\n];\nreferenceAxes.forEach(({ index, opacity }) => {\n  svg.selectAll(`.tick-label-${index}`)\n    .data(gridLevels)\n    .join(\"text\")\n    .attr(\"class\", `tick-label-${index}`)\n    .attr(\"x\", (level) => point(index, level)[0] + 6)\n    .attr(\"y\", (level) => point(index, level)[1])\n    .attr(\"fill\", t.inkSoft)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"opacity\", opacity)\n    .style(\"font-size\", \"12px\")\n    .text((level) => level);\n});\n\n// --- Category axis labels (largest-gap category emphasized) ------------------\nsvg.selectAll(\".cat-label\")\n  .data(categories)\n  .join(\"text\")\n  .attr(\"class\", \"cat-label\")\n  .attr(\"x\", (_, i) => point(i, maxValue * 1.14)[0])\n  .attr(\"y\", (_, i) => point(i, maxValue * 1.14)[1])\n  .attr(\"text-anchor\", (_, i) => anchorFor(i))\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", (_, i) => (i === maxGapIndex ? \"700\" : \"400\"))\n  .text((d) => d);\n\n// --- Gap callout: dashed connector + Δ badge on the largest-gap axis ----------\nconst gapP1 = point(maxGapIndex, gapHigh);\nconst gapP2 = point(maxGapIndex, gapLow);\n\nsvg.append(\"line\")\n  .attr(\"x1\", gapP1[0]).attr(\"y1\", gapP1[1])\n  .attr(\"x2\", gapP2[0]).attr(\"y2\", gapP2[1])\n  .attr(\"stroke\", t.amber)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"5,4\");\n\nconst badgeRadius = rScale(gapHigh) + 22;\nconst badgeAngle = angleFor(maxGapIndex);\nsvg.append(\"text\")\n  .attr(\"x\", cx + badgeRadius * Math.cos(badgeAngle))\n  .attr(\"y\", cy + badgeRadius * Math.sin(badgeAngle))\n  .attr(\"text-anchor\", anchorFor(maxGapIndex))\n  .attr(\"dominant-baseline\", \"middle\")\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"700\")\n  .style(\"paint-order\", \"stroke\")\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 4)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"fill\", t.ink)\n  .text(`Δ${gapHigh - gapLow}`);\n\n// --- Data polygons + point markers (nested data join per series) -------------\nconst radarLine = d3.line().x((d) => d[0]).y((d) => d[1]).curve(d3.curveLinearClosed);\n\nconst seriesGroups = svg.selectAll(\".series-group\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"series-group\");\n\nseriesGroups.append(\"path\")\n  .attr(\"d\", (d) => radarLine(d.values.map((v, i) => point(i, v))))\n  .attr(\"fill\", (d) => d.color)\n  .attr(\"fill-opacity\", 0.25)\n  .attr(\"stroke\", (d) => d.color)\n  .attr(\"stroke-width\", 3);\n\nseriesGroups.selectAll(\"circle\")\n  .data((d) => d.values.map((v, i) => ({ x: point(i, v)[0], y: point(i, v)[1], color: d.color })))\n  .join(\"circle\")\n  .attr(\"cx\", (d) => d.x)\n  .attr(\"cy\", (d) => d.y)\n  .attr(\"r\", 5)\n  .attr(\"fill\", (d) => d.color)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Title ------------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).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-basic · javascript · d3 · anyplot.ai\");\n\n// --- Legend (nested data join per series) -------------------------------------\nconst legendY = height - 36;\nconst legendItemWidth = 260;\nconst legendStartX = width / 2 - (series.length * legendItemWidth) / 2;\n\nconst legendGroups = svg.selectAll(\".legend-item\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (_, i) => `translate(${legendStartX + i * legendItemWidth},0)`);\n\nlegendGroups.append(\"rect\")\n  .attr(\"x\", 0).attr(\"y\", legendY - 12)\n  .attr(\"width\", 16).attr(\"height\", 16)\n  .attr(\"fill\", (d) => d.color);\n\nlegendGroups.append(\"text\")\n  .attr(\"x\", 24).attr(\"y\", legendY)\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"dominant-baseline\", \"middle\")\n  .style(\"font-size\", \"16px\")\n  .text((d) => d.name);\n"}