UI Components Easy
Donut Chart — Product Categories
A donut chart showing product category distribution with icon header, colored segments, animated arcs, and a clean legend. Ideal for shipping, inventory, or sales dashboards.
Open in Lab
MCP
vanilla-js svg
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #f0f0f0;
--surface: #ffffff;
--text: #1a1a2e;
--text-muted: #6b7280;
--accent: #2563eb;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.card {
background: var(--surface);
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 28px 32px 24px;
max-width: 380px;
width: 100%;
position: relative;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
position: relative;
padding-left: 16px;
}
.accent-line {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 32px;
border-radius: 2px;
background: var(--accent);
}
.header-icon {
width: 28px;
height: 28px;
flex-shrink: 0;
color: var(--accent);
}
.header-icon svg {
width: 100%;
height: 100%;
}
.card-title {
font-size: 1.05rem;
font-weight: 700;
color: var(--text);
}
.chart-area {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.donut-wrap {
position: relative;
flex-shrink: 0;
}
.donut-svg {
display: block;
}
.donut-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}
.donut-center-val {
font-size: 1.5rem;
font-weight: 800;
color: var(--text);
}
.donut-center-lbl {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.donut-slice {
cursor: pointer;
transition: transform .2s ease;
transform-origin: center;
transform-box: fill-box;
}
.donut-slice:hover {
transform: scale(1.06);
}
.legend {
display: flex;
flex-direction: column;
gap: 10px;
padding-left: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.82rem;
}
.legend-swatch {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.legend-label {
flex: 1;
color: var(--text);
}
.legend-pct {
font-weight: 700;
color: var(--text-muted);
font-size: 0.75rem;
margin-left: auto;
}
.chart-tooltip {
position: fixed;
background: var(--surface);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
font-size: 0.78rem;
pointer-events: none;
z-index: 100;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
color: var(--text);
}const DATA = [
{ label: "Electronics", value: 3400, color: "#1e3a5f" },
{ label: "Clothing", value: 2600, color: "#2563eb" },
{ label: "Home & Garden", value: 1800, color: "#60a5fa" },
{ label: "Books & Media", value: 1200, color: "#93c5fd" },
{ label: "Sports", value: 900, color: "#bfdbfe" },
];
const total = DATA.reduce((a, d) => a + d.value, 0);
const donutSvg = document.getElementById("donutSvg");
const legend = document.getElementById("legend");
const centerVal = document.getElementById("donutCenterVal");
const tooltip = document.getElementById("chartTooltip");
const headerIcon = document.getElementById("headerIcon");
const SIZE = 240,
CX = SIZE / 2,
CY = SIZE / 2,
R = 100,
INNER_R = 62;
const GAP = 0.02; // small gap between slices in radians
/* Header icon — inline SVG package/box icon */
headerIcon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.5 9.4l-9-5.19"/>
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>`;
/* Center total */
centerVal.textContent = total.toLocaleString();
function polarToXY(cx, cy, r, angle) {
return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
}
function arcPath(cx, cy, r, innerR, startAngle, endAngle) {
const [sx, sy] = polarToXY(cx, cy, r, startAngle);
const [ex, ey] = polarToXY(cx, cy, r, endAngle);
const [ix, iy] = polarToXY(cx, cy, innerR, endAngle);
const [ox, oy] = polarToXY(cx, cy, innerR, startAngle);
const large = endAngle - startAngle > Math.PI ? 1 : 0;
return `M${sx},${sy} A${r},${r} 0 ${large},1 ${ex},${ey} L${ix},${iy} A${innerR},${innerR} 0 ${large},0 ${ox},${oy} Z`;
}
function draw() {
donutSvg.setAttribute("viewBox", `0 0 ${SIZE} ${SIZE}`);
donutSvg.setAttribute("width", SIZE);
donutSvg.setAttribute("height", SIZE);
donutSvg.innerHTML = "";
let angle = -Math.PI / 2;
DATA.forEach((d, i) => {
const share = (d.value / total) * 2 * Math.PI;
const startAngle = angle + GAP / 2;
const endAngle = angle + share - GAP / 2;
angle += share;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", arcPath(CX, CY, R, INNER_R, startAngle, endAngle));
path.setAttribute("fill", d.color);
path.setAttribute("class", "donut-slice");
path.style.animation = `sliceReveal .5s ${i * 0.08}s ease both`;
path.addEventListener("mouseenter", (e) => showTooltip(e, d));
path.addEventListener("mousemove", (e) => posTooltip(e));
path.addEventListener("mouseleave", () => {
tooltip.hidden = true;
});
donutSvg.appendChild(path);
});
/* Build legend */
legend.innerHTML = "";
DATA.forEach((d) => {
const pct = ((d.value / total) * 100).toFixed(1);
const item = document.createElement("div");
item.className = "legend-item";
item.innerHTML = `
<span class="legend-swatch" style="background:${d.color}"></span>
<span class="legend-label">${d.label}</span>
<span class="legend-pct">${pct}%</span>`;
legend.appendChild(item);
});
}
function showTooltip(e, d) {
const pct = ((d.value / total) * 100).toFixed(1);
tooltip.innerHTML = `<strong>${d.label}</strong><br/>${d.value.toLocaleString()} items · ${pct}%`;
tooltip.hidden = false;
posTooltip(e);
}
function posTooltip(e) {
tooltip.style.left = e.clientX + 14 + "px";
tooltip.style.top = e.clientY - 42 + "px";
}
/* Inject animation keyframes */
const style = document.createElement("style");
style.textContent = `
@keyframes sliceReveal {
from { opacity: 0; transform: scale(.85); }
to { opacity: 1; transform: none; }
}`;
document.head.appendChild(style);
draw();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Donut Chart — Product Categories</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="card">
<div class="card-header">
<div class="accent-line"></div>
<div class="header-icon" id="headerIcon"></div>
<h2 class="card-title">Products Shipped</h2>
</div>
<div class="chart-area">
<div class="donut-wrap" id="donutWrap">
<svg id="donutSvg" class="donut-svg" aria-label="Donut chart"></svg>
<div class="donut-center" id="donutCenter">
<div class="donut-center-val" id="donutCenterVal"></div>
<div class="donut-center-lbl">Total</div>
</div>
</div>
</div>
<div class="legend" id="legend"></div>
<div class="chart-tooltip" id="chartTooltip" hidden></div>
</div>
<script src="script.js"></script>
</body>
</html>Features
- Donut chart — five colored arc segments with a hollow center
- Animated arcs — slices reveal with a staggered fade-in animation
- Hover highlight — segment scales up on hover for emphasis
- Icon header — pink/magenta accent line with an inline SVG package icon
- Dynamic legend — color-coded labels and percentages built from data
How it works
- A
DATAarray defines five product categories with labels, values, and colors - SVG
<path>arcs are computed usingpolarToXY()andarcPath()helpers - Each arc animates in with a staggered
fadeInkeyframe - On hover, the active slice gets
transform: scale(1.06)via CSS - The legend is generated dynamically, showing colored swatches and percentage values