{"spec_id":"radar-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// radar-basic: Basic Radar Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-24\n//# anyplot-orientation: square\n\n// Only the core `highcharts` bundle is loaded (no highcharts-more), so the\n// native polar/radar series type isn't available. Instead we project each\n// axis's polar coordinate to Cartesian ourselves and draw the grid, spokes,\n// labels, and data polygons with the core SVG renderer — same technique the\n// gauge-basic implementation uses for its needle overlay.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (Employee performance review, two review cycles) -----------------\nconst categories = [\n  \"Communication\",\n  \"Technical Skills\",\n  \"Teamwork\",\n  \"Leadership\",\n  \"Problem Solving\",\n  \"Adaptability\",\n];\nconst series = [\n  { name: \"Q3 Review\", color: t.palette[0], values: [78, 85, 90, 65, 72, 80] },\n  { name: \"Q4 Review\", color: t.palette[1], values: [88, 82, 92, 75, 85, 78] },\n];\nconst MAX_VALUE = 100;\nconst RING_LEVELS = [20, 40, 60, 80, 100];\nconst n = categories.length;\n\nconst TITLE = \"radar-basic · javascript · highcharts · anyplot.ai\";\nconst titleFs = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length))) + \"px\";\n\n// --- Chart (empty core chart used as a canvas for the renderer overlay) ----\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    margin: [110, 80, 130, 80],\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: titleFs, fontWeight: \"600\" },\n  },\n  xAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  yAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },\n  legend: { enabled: false },\n  tooltip: {\n    enabled: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink },\n    formatter() {\n      return `<b>${this.series.name}</b><br/>${this.point.name}: ${this.point.custom.actualValue}`;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\n// Fix the (visible: false) axes to a known pixel-space extent so a real\n// scatter series can be data-bound at the same polar-projected coordinates\n// the renderer overlay uses below — this keeps the PNG output identical\n// while giving the interactive HTML genuine Highcharts series/tooltip usage.\nchart.xAxis[0].setExtremes(0, chart.plotWidth, false);\nchart.yAxis[0].setExtremes(0, chart.plotHeight, false);\n\n// --- Geometry ----------------------------------------------------------------\nconst cx = chart.plotLeft + chart.plotWidth / 2;\nconst cy = chart.plotTop + chart.plotHeight / 2;\nconst outerR = Math.min(chart.plotWidth, chart.plotHeight) / 2 - 70;\n\n// angle 0 points straight up, axes proceed clockwise\nconst angleOf = (i) => -Math.PI / 2 + i * ((2 * Math.PI) / n);\nconst pointAt = (i, radiusFrac) => {\n  const angle = angleOf(i);\n  return [cx + outerR * radiusFrac * Math.cos(angle), cy + outerR * radiusFrac * Math.sin(angle)];\n};\n\n// --- Grid rings --------------------------------------------------------------\nRING_LEVELS.forEach((level) => {\n  const radiusFrac = level / MAX_VALUE;\n  const path = [\"M\"];\n  for (let i = 0; i < n; i += 1) {\n    const [x, y] = pointAt(i, radiusFrac);\n    path.push(...(i === 0 ? [x, y] : [\"L\", x, y]));\n  }\n  path.push(\"Z\");\n  chart.renderer\n    .path(path)\n    .attr({ stroke: t.grid, \"stroke-width\": 1, fill: \"none\", zIndex: 1 })\n    .add();\n});\n\n// --- Spokes + axis labels -----------------------------------------------------\ncategories.forEach((label, i) => {\n  const [ox, oy] = pointAt(i, 1);\n  chart.renderer\n    .path([\"M\", cx, cy, \"L\", ox, oy])\n    .attr({ stroke: t.inkSoft, \"stroke-width\": 1, zIndex: 1 })\n    .add();\n\n  const angle = angleOf(i);\n  const cos = Math.cos(angle);\n  const sin = Math.sin(angle);\n  const [lx, ly] = pointAt(i, 1 + 26 / outerR);\n  const align = cos > 0.3 ? \"left\" : cos < -0.3 ? \"right\" : \"center\";\n  chart.renderer\n    .text(label, lx, ly + sin * 6 + 5)\n    .attr({ align, zIndex: 5 })\n    .css({ fontSize: \"15px\", fontWeight: \"600\", color: t.ink, fontFamily: \"inherit\" })\n    .add();\n});\n\n// --- Ring scale labels (along the top spoke) ---------------------------------\nRING_LEVELS.forEach((level) => {\n  const [, ly] = pointAt(0, level / MAX_VALUE);\n  chart.renderer\n    .text(String(level), cx + 8, ly + 4)\n    .attr({ align: \"left\", zIndex: 4 })\n    .css({ fontSize: \"12px\", color: t.inkSoft, fontFamily: \"inherit\" })\n    .add();\n});\n\n// --- Data polygons -------------------------------------------------------------\nseries.forEach((s) => {\n  const vertices = s.values.map((value, i) => pointAt(i, value / MAX_VALUE));\n  const path = [\"M\"];\n  vertices.forEach(([x, y], i) => path.push(...(i === 0 ? [x, y] : [\"L\", x, y])));\n  path.push(\"Z\");\n\n  chart.renderer\n    .path(path)\n    .attr({\n      fill: s.color,\n      \"fill-opacity\": 0.22,\n      stroke: s.color,\n      \"stroke-width\": 2.5,\n      \"stroke-linejoin\": \"round\",\n      zIndex: 3,\n    })\n    .add();\n\n  vertices.forEach(([x, y]) => {\n    chart.renderer\n      .circle(x, y, 6)\n      .attr({ fill: s.color, stroke: t.pageBg, \"stroke-width\": 1.5, zIndex: 4 })\n      .add();\n  });\n\n  // Real Highcharts scatter series at the same vertex pixels (mapped through\n  // the fixed-extent axes above) — markers stay hidden so the PNG is\n  // untouched, but each point is genuinely data-bound and hoverable in the\n  // interactive HTML, with the tooltip reporting the actual category/value.\n  chart.addSeries(\n    {\n      type: \"scatter\",\n      name: s.name,\n      color: s.color,\n      enableMouseTracking: true,\n      stickyTracking: false,\n      animation: false,\n      marker: {\n        enabled: false,\n        radius: 7,\n        states: { hover: { enabled: true, radius: 7, lineWidth: 1.5, lineColor: t.pageBg } },\n      },\n      data: vertices.map(([x, y], i) => ({\n        x: x - chart.plotLeft,\n        y: chart.plotTop + chart.plotHeight - y,\n        name: categories[i],\n        custom: { actualValue: s.values[i] },\n      })),\n    },\n    false\n  );\n});\nchart.redraw();\n\n// --- Legend ---------------------------------------------------------------\nconst chipSize = 14;\nconst gap = 28;\nconst legendFs = 15;\nconst chipTextWidths = series.map((s) => s.name.length * 8.2);\nconst legendWidth = series.reduce(\n  (sum, _s, i) => sum + chipSize + 10 + chipTextWidths[i] + (i < series.length - 1 ? gap : 0),\n  0\n);\nlet legendX = cx - legendWidth / 2;\nconst legendY = chart.plotTop + chart.plotHeight + 60;\n\nseries.forEach((s, i) => {\n  chart.renderer\n    .rect(legendX, legendY - chipSize / 2, chipSize, chipSize, 3)\n    .attr({ fill: s.color, zIndex: 5 })\n    .add();\n  chart.renderer\n    .text(s.name, legendX + chipSize + 10, legendY + chipSize / 2 - 2)\n    .attr({ align: \"left\", zIndex: 5 })\n    .css({ fontSize: `${legendFs}px`, color: t.ink, fontFamily: \"inherit\" })\n    .add();\n  legendX += chipSize + 10 + chipTextWidths[i] + gap;\n});\n"}