I make dashboards for work. Quick stuff. Clean, readable, no drama. Last month I needed a donut chart fast. Actually, I needed four. I tried Chart.js, D3.js, ApexCharts, and ECharts. I used each one in a real page, and I kept notes like a snack-obsessed squirrel. For a side-by-side look at how these options stack up, this comprehensive comparison of JavaScript charting libraries, including Chart.js, D3.js, ApexCharts, and ECharts digs into features, performance, and common use cases.
In case you’d like a separate, blow-by-blow narrative of building donut charts from scratch, I documented every step in this detailed write-up.
You know what? They all work. But they feel different in the hand.
Quick take: which one should you grab?
- Chart.js: Easy, fast, and friendly. Great for most cases.
- D3.js: Full control. You can shape every pixel. More code though.
- ApexCharts: Pretty labels and a sweet total number in the center.
- ECharts: Heavy, powerful, and smooth with big data.
Need an even more specialized option? EJSChart ships focused donut and pie charts with zero-config defaults.
Earlier, I also put seven different JavaScript chart libraries through the wringer; you can skim the full comparison in I tried 7 JavaScript chart libraries—here’s what actually worked.
I’ll show you the exact code I ran and what tripped me up.
Chart.js — my fast win
I reached for Chart.js first because I was tired and hungry. It took me five minutes, maybe less. The defaults looked good. Hover felt smooth. The legend didn’t fight me. That alone was a relief.
For an even simpler, plug-and-play approach, I once pulled together a set of examples that show how to spin up charts with almost no configuration, which you can read in Easy JavaScript charts – what actually worked for me.
What I ran (Chart.js 4)
HTML
<canvas id="donut1" aria-label="Sales by channel donut chart" role="img"></canvas>
JavaScript
import { Chart, ArcElement, Tooltip, Legend } from 'chart.js';
Chart.register(ArcElement, Tooltip, Legend);
const ctx = document.getElementById('donut1');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Web', 'Retail', 'Wholesale', 'Partners'],
datasets: [
{
data: [48, 22, 18, 12],
backgroundColor: ['#4F46E5', '#22C55E', '#F59E0B', '#EF4444'],
borderWidth: 0
}
]
},
options: {
cutout: '60%',
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.label}: ${ctx.formattedValue}%`
}
}
},
animation: { animateRotate: true, animateScale: true }
}
});
What I liked
- Setup felt painless.
- Colors and legend looked clean out of the box.
- Works fine in a small card layout.
What bugged me
- The legend can wrap in narrow layouts. I had to tweak CSS.
- If you need fancy center labels, you’ll need a plugin or custom draw.
If animations are your jam, I unpack the quirks of spinning slices in Animated pie charts in JavaScript—what worked, what didn’t.
D3.js — full control, strong coffee
I love D3. But let’s be honest: it’s more code. I used it when I needed custom arcs, gentle hover fades, and a label in the middle that doesn’t look like a sticker.
What I ran (D3 v7)
HTML
<div id="donut2"></div>
JavaScript
import * as d3 from 'd3';
const data = [
{ label: 'Marketing', value: 35 },
{ label: 'Dev', value: 40 },
{ label: 'Ops', value: 15 },
{ label: 'Other', value: 10 }
];
const width = 320, height = 320;
const radius = Math.min(width, height) / 2;
const color = d3.scaleOrdinal()
.domain(data.map(d => d.label))
.range(['#10B981', '#3B82F6', '#F43F5E', '#F59E0B']);
const pie = d3.pie().value(d => d.value).sort(null);
const arc = d3.arc().innerRadius(radius * 0.6).outerRadius(radius - 2);
const svg = d3.select('#donut2')
.append('svg')
.attr('width', width)
.attr('height', height)
.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))
.attr('opacity', 0.95)
.on('mouseover', function () { d3.select(this).attr('opacity', 0.8); })
.on('mouseout', function () { d3.select(this).attr('opacity', 0.95); });
svg.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.attr('font-size', '16px')
.attr('fill', '#111827')
.text('Team Spend');
What I liked
- It bends to your will. Every arc, every label.
- Transitions feel silky.
- I can add center text and custom legends with no hacks.
What bugged me
- More code. More time.
- You manage layout and a11y on your own.
ApexCharts — sweet center labels
One reason I reached for ApexCharts: donut labels. That total in the middle looks great on a KPI card. And the defaults? Polished. I shipped a report page with it in a single morning.
What I ran
HTML
<div id="donut3"></div>
JavaScript
const options = {
series: [44, 55, 13, 33],
labels: ['Apples', 'Bananas', 'Cherries', 'Dates'],
chart: { type: 'donut', width: 320 },
colors: ['#7C3AED', '#22C55E', '#F97316', '#06B6D4'],
plotOptions: {
pie: {
donut: {
size: '60%',
labels: {
show: true,
name: { show: true },
value: { show: true },
total: {
show: true,
label: 'Total',
formatter: () => '145'
}
}
}
}
},
legend: { position: 'bottom' },
dataLabels: { enabled: true }
};
const chart = new ApexCharts(document.querySelector('#donut3'), options);
chart.render();
What I liked
- Center total label out of the box.
- Fast to theme and ship.
- Good on mobile.
What bugged me
- The API has many knobs. I had to test small things to get spacing right.
ECharts — heavy, strong, and smooth under load
I used ECharts for a big dashboard that ran on a TV screen in our hallway. Lots of data, long sessions. It held up. The tooltips are rich. The theme system is deep.
What I ran
HTML
<div id="donut4" style="width: 320px; height: 320px;"></div>
JavaScript
“`javascript
const chart = echarts.init(document.getElementById('donut4'));
chart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0 },
series: [
{
name: 'Traffic',
type: 'pie',
radius: ['55%', '75%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, position: 'outside' },
data: [
{ value: 1048, name: 'Direct' },
{ value: 735, name: 'Email' },
{ value: 580, name: 'Search' },
{ value: 484, name: 'Social' }
]
}
