I built radar charts in JavaScript so you don’t have to guess

I’m Kayla, and I actually used these. I needed radar charts for a team skills map and a snack-tasting game. Weird mix, I know. But that’s real life. I tried Chart.js, Apache ECharts, ApexCharts, and even a tiny D3 build. Some nights it was smooth. Some nights… not so much. Here’s what worked for me, with real code I ran. If you want a blow-by-blow narrative of that first adventure, check out I built radar charts in JavaScript so you don’t have to guess.

Why a radar chart, anyway?

I had five skills to compare for two people on my team. Later, I mapped flavor notes for chips. Sweet, salty, spicy, sour, umami. A radar chart shows that shape fast. Your eye sees gaps right away. It feels like a spiderweb story.

You know what? When it works, it clicks.


Chart.js: fast start, friendly setup

Chart.js felt like a warm hoodie. It’s simple. I used it first because I could get a chart on the page in minutes. I used version 4.x from npm once and a CDN another time.

What I liked:

  • Easy data model
  • Good defaults
  • Decent docs, even for kids learning

If you’re new to how Chart.js draws that spider-web shape, the official radar chart guide walks through every option you can toggle—from line tension to point styling.

What bugged me:

  • Tooltips got jumpy on small phones
  • Legend wrap got messy
  • Multi-axis tweaks on the radial scale felt tight

Need deeper control over those axis lines? The more advanced knobs live in the radial axis reference, and they saved me a few “why is this grid so dark?” moments.

Here’s a real snippet I used for a team skills map:

<canvas id="skills"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
  const ctx = document.getElementById('skills');

  new Chart(ctx, {
    type: 'radar',
    data: {
      labels: ['Speed', 'Strength', 'Skill', 'Stamina', 'Teamwork'],
      datasets: [
        {
          label: 'Player A',
          data: [60, 80, 70, 50, 90],
          borderColor: '#3b82f6',
          backgroundColor: 'rgba(59,130,246,0.2)',
          pointRadius: 3
        },
        {
          label: 'Player B',
          data: [65, 70, 85, 60, 70],
          borderColor: '#ef4444',
          backgroundColor: 'rgba(239,68,68,0.2)',
          pointRadius: 3
        }
      ]
    },
    options: {
      responsive: true,
      plugins: {
        legend: { position: 'top' },
        tooltip: { enabled: true }
      },
      scales: {
        r: {
          suggestedMin: 0,
          suggestedMax: 100,
          angleLines: { color: '#e5e7eb' },
          grid: { color: '#e5e7eb' },
          pointLabels: { color: '#111827' }
        }
      }
    }
  });
</script>

Tip: keep pointRadius small. It looks cleaner and helps on touch screens.


ECharts: flashy and packed with features

When I needed more control—like showing max values per axis—I went with Apache ECharts. It’s rich. The radar has “indicator” items, each with a name and a max. I used this for a snack-tasting night with my kids. Yes, we charted hot chips. It was silly and fun. For a broader comparison of spider-chart libraries, I logged everything that actually worked in this deep dive.

What I liked:

  • Per-axis max values
  • Pretty split areas and circle shape
  • Good resize behavior

What bugged me:

  • Big bundle if you import the whole thing
  • Options get nested and long
  • Styling takes a bit of patience

Here’s my real snack chart:

<div id="snacks" style="height:380px;"></div>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<script>
  const el = document.getElementById('snacks');
  const chart = echarts.init(el);

  const options = {
    tooltip: {},
    radar: {
      shape: 'circle',
      indicator: [
        { name: 'Sweet', max: 10 },
        { name: 'Salty', max: 10 },
        { name: 'Spicy', max: 10 },
        { name: 'Sour',  max: 10 },
        { name: 'Umami', max: 10 }
      ],
      splitArea: {
        areaStyle: { color: ['#f9fafb', '#eef2ff'] }
      }
    },
    series: [
      {
        type: 'radar',
        data: [
          { value: [2, 6, 9, 1, 5], name: 'Hot Chips' },
          { value: [5, 3, 1, 2, 7], name: 'Seaweed Crisps' }
        ],
        areaStyle: { opacity: 0.2 },
        lineStyle: { width: 2 }
      }
    ]
  };

  chart.setOption(options);
  window.addEventListener('resize', () => chart.resize());
</script>

Little note: if you care about size, import from echarts/core with only the pieces you need.


ApexCharts: quick dashboards, nice polish

ApexCharts felt like a sweet spot for app dashboards. I used it in a panel with other charts. It looked modern without too much fuss.

What I liked:

  • Smooth markers and fills
  • Simple legend and labels
  • Export image was easy

What bugged me:

  • Long labels could clip
  • Heavy shadows slowed an old Chromebook I keep at home

Here’s a sample I used in a Vue page, but this is plain JS:

<div id="radar"></div>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
  const options = {
    chart: { type: 'radar', height: 360 },
    series: [
      { name: 'A', data: [60, 80, 70, 50, 90] },
      { name: 'B', data: [65, 70, 85, 60, 70] }
    ],
    labels: ['Speed', 'Strength', 'Skill', 'Stamina', 'Teamwork'],
    stroke: { width: 2 },
    fill: { opacity: 0.2 },
    markers: { size: 4 }
  };

  new ApexCharts(document.querySelector('#radar'), options).render();
</script>

If labels clip, I bump the chart height or trim names a bit. Not fancy, but it works.


D3: when you want full control (and time)

One late fall weekend, I wrote a tiny radar with D3. It gave me full control over axes, labels, and keyboard focus. But yes, it took a while.

What I liked:

  • Total control
  • Custom scales per axis
  • Smooth transitions

What bugged me:

  • Lots of code for simple things
  • You own all the layout and resize

Here’s a tiny base I used. It’s not a full chart, but it shows the core idea:

“`html

const w = 420, h = 420, cx = w/2, cy = h/2;
const axes = [‘Speed’,’Strength’,’Skill’,’Stamina’,’Teamwork’];
const values = [60, 80, 70, 50, 90];
const max = 100;

const r = d3.scaleLinear().domain([0, max]).range([0, 160]);

const angle = d3.scaleLinear().domain([0, axes.length]).range([0, Math.PI * 2]);

const points = axes.map((a, i) => {
const ang = angle(i);
return [cx + r(values[i]) * Math.cos(ang), cy + r(values[i]) * Math.sin(ang)];
});

const svg = d3.select(‘#radarD3’);
// grid circles
[20,40,60,80,100].forEach(t => {
svg.append(‘circle’)
.attr(‘cx’, cx).attr(‘cy’, cy).attr(‘r’, r(t))
.attr(‘fill’, ‘none’).attr(‘stroke’, ‘#e5e7eb’);
});
// polygon
svg.append(‘polygon’)
.attr(‘points’, points.map(p => p.join(‘,’)).join(‘ ‘))
.attr(‘fill’, ‘rgba(59,130,246,0.2)’)
.attr(‘stroke’, ‘#3b82f6’).