I Built 5 JavaScript Donut Charts. Here’s What Actually Worked.

I’m Kayla, and yep, I made a bunch of donut charts last month. Work dashboards. A school fundraiser page. Even my own food log. I tried five tools: Chart.js, D3, ApexCharts, ECharts, and Highcharts. For a deeper side-by-side look at these libraries, check out this comprehensive comparison of JavaScript charting libraries.
Since then I’ve also kept my eye on a newer lightweight option, EJSChart, which promises the same smooth donuts with a fraction of the setup.

If you’d like an even deeper dive—including bundle sizes, build notes, and a few extra “gotchas” I skipped here—check out the full comparison post where I document every test run.

You know what? A donut chart looks simple. But little details matter. Labels wrap. Colors clash. Center text gets wonky. I learned the hard way, so you don’t have to.


What I needed, in plain words

  • Fast setup for a sales dashboard with 4–6 slices
  • Clean labels that don’t pile up on mobile
  • A center label that says “Total” or a short note
  • Good colors that work in dark mode
  • OK performance on a cheap Android phone
  • Screen reader text, at least the basics

Feeling short on interesting sample data to test your next donut? I recently played with dating-app response stats pulled from PlanCulFacile — they publish anonymised match and message metrics you can download as CSV, perfect for stress-testing any of the chart libraries below with a fresh, real-world dataset. For something with a bit more geographical spice, the classifieds-style dataset over at Backpage Crystal breaks down listing counts by city and category, giving you granular numbers you can segment into eye-catching donut slices.

I also cared about bundle size a bit, but not more than speed and clarity. If you’re hunting for benchmarks to back up your own choices, there’s an in-depth analysis of the performance and scalability of various JavaScript charting libraries that’s worth a skim.


Chart.js: my quick win (and my go-to)

I used Chart.js for a sales donut in our store dashboard. I had coffee, ten minutes, and a messy CSV. It still worked. It was fast, and the look was clean.

What I liked:

  • Very quick start
  • Colors look nice with simple tweaks
  • Animations feel smooth
  • Works well with React and plain HTML

What bugged me:

  • No built-in center text (you can add a tiny plugin)
  • Legends can wrap odd on very small screens

Here’s the exact snippet I used for “Sales by Category” (four slices). Took me under ten minutes, start to finish.

<canvas id="sales"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('sales');
new Chart(ctx, {
  type: 'doughnut',
  data: {
    labels: ['Shoes','Hats','Bags','Socks'],
    datasets: [{
      data: [45, 25, 20, 10],
      backgroundColor: ['#4F46E5', '#06B6D4', '#10B981', '#F59E0B'],
      borderWidth: 0
    }]
  },
  options: {
    cutout: '60%',
    plugins: {
      legend: { position: 'bottom' },
      tooltip: {
        callbacks: {
          label: (ctx) => {
            const total = ctx.dataset.data.reduce((a,b)=>a+b, 0);
            const val = ctx.parsed;
            const pct = Math.round((val/total)*100);
            return `${ctx.label}: ${val} (${pct}%)`;
          }
        }
      }
    },
    animation: { animateRotate: true, duration: 700 }
  },
  plugins: [{
    id: 'centerText',
    beforeDraw(chart) {
      const {ctx, chartArea} = chart;
      if (!chartArea) return;
      const x = (chartArea.left + chartArea.right) / 2;
      const y = (chartArea.top + chartArea.bottom) / 2;
      ctx.save();
      ctx.font = '600 14px system-ui';
      ctx.fillStyle = '#334155';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('Sales', x, y);
      ctx.restore();
    }
  }]
});
</script>

Speed to first chart: about 5 minutes. Not joking.
Dark mode tip: switch text to lighter gray and nudge colors up a notch.


D3: full control, more work

This one is for folks who like to tinker. I used D3 for a fitness page where I needed exact sizes and a custom arc. I wanted a label in the center and a small legend at the top. No fluff.

What I liked:

  • Pixel-level control
  • Great for custom labels and arcs
  • Scales and color control feel pro

What bugged me:

  • More code
  • You must add ARIA and keyboard bits yourself

This is the “Macros” donut I built for my own meal plan:

<div id="macro"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script>
const data = [
  { label: 'Protein', value: 32 },
  { label: 'Carbs', value: 48 },
  { label: 'Fat', value: 20 }
];

const width = 260, height = 260, r = 110;
const color = d3.scaleOrdinal().range(['#EF4444','#3B82F6','#10B981']);
const arc = d3.arc().innerRadius(65).outerRadius(r);
const pie = d3.pie().value(d => d.value);

const svg = d3.select('#macro').append('svg')
  .attr('viewBox', `0 0 ${width} ${height}`)
  .attr('role', 'img')
  .attr('aria-label', 'Daily macros donut chart')
  .append('g')
  .attr('transform', `translate(${width/2},${height/2})`);

svg.selectAll('path')
  .data(pie(data))
  .enter().append('path')
  .attr('d', arc)
  .attr('fill', d => color(d.data.label));

svg.append('text')
  .attr('text-anchor','middle')
  .attr('dy','0.35em')
  .style('font-weight','600')
  .text('Macros');
</script>

If you need the chart to say exactly what you see in your head, D3 hits the mark. But yes, it takes more time.


ApexCharts: neat defaults, nice center labels

I used ApexCharts for a budget report because it showed a “total” in the hole with no fuss. The legends felt tidy. The default font looked sharp too.

What I liked:

  • Center labels built in
  • Simple config for tooltips and legends
  • Handy responsive settings

What bugged me:

  • Some presets look “heavy” with too many effects
  • Need to tone down labels on tiny screens

Real budget example:

<div id="budget"></div>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
var options = {
  chart: { type: 'donut', height: 260 },
  series: [1200, 900, 300],
  labels: ['Rent','Food','Fun'],
  colors: ['#6366F1', '#22C55E', '#F97316'],
  dataLabels: { enabled: false },
  legend: { position: 'bottom' },
  plotOptions: {
    pie: {
      donut: {
        size: '60%',
        labels: { show: true, total: { show: true, label: 'Budget' } }
      }
    }
  },
  tooltip: { y: { formatter: val => '
#039; + val } },
  responsive: [{
    breakpoint: 480,
    options: { legend: { position: 'bottom' } }
  }]
};
new ApexCharts(document.querySelector('#budget'), options).render();
</script>

My boss liked this one best. The “Budget” total in the center sold it.


ECharts: rich, bold, and fast on mobile

For a school bake sale site, I used ECharts. It looked fancy with very little work, and it ran smooth on an old tablet. The theme support helped me match the school colors.

What I liked:

  • Very smooth on touch devices
  • Strong visuals
  • Good tooltip format out of the box

What bugged me:

  • The config can feel long
  • Labels can get busy if you show them all

Bake sale orders by channel:

“`html

var chart = echarts.init(document.getElementById(‘orders’));
var option = {
tooltip: { trigger: ‘item’, formatter: ‘{b}: {c} ({d}%)’ },
legend: { bottom: 0