Basic Radar Chart — Highcharts

A radar chart (also known as spider or web chart) displays multivariate data on axes starting from a common center point, with values connected to form a polygon. Each axis represents a different variable, making it ideal for comparing multiple quantitative variables at once or visualizing strengths and weaknesses across categories.

Basic Radar Chart rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// radar-basic: Basic Radar Chart
// Library: highcharts 12.6.0 | JavaScript 22.23.1
// Quality: 91/100 | Created: 2026-07-24
//# anyplot-orientation: square

// Only the core `highcharts` bundle is loaded (no highcharts-more), so the
// native polar/radar series type isn't available. Instead we project each
// axis's polar coordinate to Cartesian ourselves and draw the grid, spokes,
// labels, and data polygons with the core SVG renderer — same technique the
// gauge-basic implementation uses for its needle overlay.
const t = window.ANYPLOT_TOKENS;

// --- Data (Employee performance review, two review cycles) -----------------
const categories = [
  "Communication",
  "Technical Skills",
  "Teamwork",
  "Leadership",
  "Problem Solving",
  "Adaptability",
];
const series = [
  { name: "Q3 Review", color: t.palette[0], values: [78, 85, 90, 65, 72, 80] },
  { name: "Q4 Review", color: t.palette[1], values: [88, 82, 92, 75, 85, 78] },
];
const MAX_VALUE = 100;
const RING_LEVELS = [20, 40, 60, 80, 100];
const n = categories.length;

const TITLE = "radar-basic · javascript · highcharts · anyplot.ai";
const titleFs = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length))) + "px";

// --- Chart (empty core chart used as a canvas for the renderer overlay) ----
const chart = Highcharts.chart("container", {
  chart: {
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
    margin: [110, 80, 130, 80],
  },
  credits: { enabled: false },
  title: {
    text: TITLE,
    style: { color: t.ink, fontSize: titleFs, fontWeight: "600" },
  },
  xAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },
  yAxis: { visible: false, gridLineWidth: 0, lineWidth: 0, tickLength: 0 },
  legend: { enabled: false },
  tooltip: {
    enabled: true,
    backgroundColor: t.elevatedBg,
    borderColor: t.grid,
    style: { color: t.ink },
    formatter() {
      return `<b>${this.series.name}</b><br/>${this.point.name}: ${this.point.custom.actualValue}`;
    },
  },
  plotOptions: { series: { animation: false } },
  series: [],
});

// Fix the (visible: false) axes to a known pixel-space extent so a real
// scatter series can be data-bound at the same polar-projected coordinates
// the renderer overlay uses below — this keeps the PNG output identical
// while giving the interactive HTML genuine Highcharts series/tooltip usage.
chart.xAxis[0].setExtremes(0, chart.plotWidth, false);
chart.yAxis[0].setExtremes(0, chart.plotHeight, false);

// --- Geometry ----------------------------------------------------------------
const cx = chart.plotLeft + chart.plotWidth / 2;
const cy = chart.plotTop + chart.plotHeight / 2;
const outerR = Math.min(chart.plotWidth, chart.plotHeight) / 2 - 70;

// angle 0 points straight up, axes proceed clockwise
const angleOf = (i) => -Math.PI / 2 + i * ((2 * Math.PI) / n);
const pointAt = (i, radiusFrac) => {
  const angle = angleOf(i);
  return [cx + outerR * radiusFrac * Math.cos(angle), cy + outerR * radiusFrac * Math.sin(angle)];
};

// --- Grid rings --------------------------------------------------------------
RING_LEVELS.forEach((level) => {
  const radiusFrac = level / MAX_VALUE;
  const path = ["M"];
  for (let i = 0; i < n; i += 1) {
    const [x, y] = pointAt(i, radiusFrac);
    path.push(...(i === 0 ? [x, y] : ["L", x, y]));
  }
  path.push("Z");
  chart.renderer
    .path(path)
    .attr({ stroke: t.grid, "stroke-width": 1, fill: "none", zIndex: 1 })
    .add();
});

// --- Spokes + axis labels -----------------------------------------------------
categories.forEach((label, i) => {
  const [ox, oy] = pointAt(i, 1);
  chart.renderer
    .path(["M", cx, cy, "L", ox, oy])
    .attr({ stroke: t.inkSoft, "stroke-width": 1, zIndex: 1 })
    .add();

  const angle = angleOf(i);
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);
  const [lx, ly] = pointAt(i, 1 + 26 / outerR);
  const align = cos > 0.3 ? "left" : cos < -0.3 ? "right" : "center";
  chart.renderer
    .text(label, lx, ly + sin * 6 + 5)
    .attr({ align, zIndex: 5 })
    .css({ fontSize: "15px", fontWeight: "600", color: t.ink, fontFamily: "inherit" })
    .add();
});

// --- Ring scale labels (along the top spoke) ---------------------------------
RING_LEVELS.forEach((level) => {
  const [, ly] = pointAt(0, level / MAX_VALUE);
  chart.renderer
    .text(String(level), cx + 8, ly + 4)
    .attr({ align: "left", zIndex: 4 })
    .css({ fontSize: "12px", color: t.inkSoft, fontFamily: "inherit" })
    .add();
});

// --- Data polygons -------------------------------------------------------------
series.forEach((s) => {
  const vertices = s.values.map((value, i) => pointAt(i, value / MAX_VALUE));
  const path = ["M"];
  vertices.forEach(([x, y], i) => path.push(...(i === 0 ? [x, y] : ["L", x, y])));
  path.push("Z");

  chart.renderer
    .path(path)
    .attr({
      fill: s.color,
      "fill-opacity": 0.22,
      stroke: s.color,
      "stroke-width": 2.5,
      "stroke-linejoin": "round",
      zIndex: 3,
    })
    .add();

  vertices.forEach(([x, y]) => {
    chart.renderer
      .circle(x, y, 6)
      .attr({ fill: s.color, stroke: t.pageBg, "stroke-width": 1.5, zIndex: 4 })
      .add();
  });

  // Real Highcharts scatter series at the same vertex pixels (mapped through
  // the fixed-extent axes above) — markers stay hidden so the PNG is
  // untouched, but each point is genuinely data-bound and hoverable in the
  // interactive HTML, with the tooltip reporting the actual category/value.
  chart.addSeries(
    {
      type: "scatter",
      name: s.name,
      color: s.color,
      enableMouseTracking: true,
      stickyTracking: false,
      animation: false,
      marker: {
        enabled: false,
        radius: 7,
        states: { hover: { enabled: true, radius: 7, lineWidth: 1.5, lineColor: t.pageBg } },
      },
      data: vertices.map(([x, y], i) => ({
        x: x - chart.plotLeft,
        y: chart.plotTop + chart.plotHeight - y,
        name: categories[i],
        custom: { actualValue: s.values[i] },
      })),
    },
    false
  );
});
chart.redraw();

// --- Legend ---------------------------------------------------------------
const chipSize = 14;
const gap = 28;
const legendFs = 15;
const chipTextWidths = series.map((s) => s.name.length * 8.2);
const legendWidth = series.reduce(
  (sum, _s, i) => sum + chipSize + 10 + chipTextWidths[i] + (i < series.length - 1 ? gap : 0),
  0
);
let legendX = cx - legendWidth / 2;
const legendY = chart.plotTop + chart.plotHeight + 60;

series.forEach((s, i) => {
  chart.renderer
    .rect(legendX, legendY - chipSize / 2, chipSize, chipSize, 3)
    .attr({ fill: s.color, zIndex: 5 })
    .add();
  chart.renderer
    .text(s.name, legendX + chipSize + 10, legendY + chipSize / 2 - 2)
    .attr({ align: "left", zIndex: 5 })
    .css({ fontSize: `${legendFs}px`, color: t.ink, fontFamily: "inherit" })
    .add();
  legendX += chipSize + 10 + chipTextWidths[i] + gap;
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/radar-basic/highcharts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "radar-basic",
  "language": "javascript",
  "library": "highcharts",
  "page": "https://anyplot.ai/radar-basic/javascript/highcharts",
  "hub": "https://anyplot.ai/radar-basic",
  "code_json": "https://api.anyplot.ai/specs/radar-basic/highcharts/code",
  "spec_json": "https://api.anyplot.ai/specs/radar-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/javascript/highcharts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/javascript/highcharts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/javascript/highcharts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/radar-basic/javascript/highcharts/plot-dark.html",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Radar Chart on anyplot.ai.

Other implementations