const DONUT_ICONS = {
beaker:
'',
grinder:
'',
};
const esc = (s) =>
s
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
class DonutChart extends HTMLElement {
static get observedAttributes() {
return ["data-items"];
}
connectedCallback() {
requestAnimationFrame(() => this.render());
this._themeObserver = new MutationObserver(() =>
requestAnimationFrame(() => this.render()),
);
this._themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
}
disconnectedCallback() {
this._themeObserver?.disconnect();
this._themeObserver = null;
}
attributeChangedCallback() {
if (this.isConnected) requestAnimationFrame(() => this.render());
}
render() {
const raw = this.dataset.items || "";
if (!raw) {
this.innerHTML = "";
return;
}
const items = raw
.split("|")
.map((s) => {
const idx = s.lastIndexOf(":");
if (idx === -1) return null;
const label = s.slice(0, idx).trim();
const count = parseInt(s.slice(idx + 1), 10);
return label && count > 0 ? { label, count } : null;
})
.filter(Boolean);
if (items.length === 0) {
this.innerHTML = "";
return;
}
const total = items.reduce((sum, i) => sum + i.count, 0);
const maxCount = items[0].count;
const rgb =
getComputedStyle(document.documentElement)
.getPropertyValue("--highlight-rgb")
.trim() || "185, 28, 28";
const colorFor = (count) => {
const alpha = (0.25 + 0.75 * (count / maxCount)).toFixed(2);
return `rgba(${rgb}, ${alpha})`;
};
const size = 140;
const strokeWidth = 28;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const cx = size / 2;
const cy = size / 2;
const gapDeg = items.length > 1 ? 3 : 0;
const gapArc = (gapDeg / 360) * circumference;
let angle = 0;
const segments = items.map((item, i) => {
const fraction = item.count / total;
const arcLen = fraction * circumference;
const visible = Math.max(0, arcLen - gapArc);
const rotation = angle - 90;
angle += fraction * 360;
return ``;
});
const legend = items.map((item, i) => {
const pct = Math.round((item.count / total) * 100);
return `
${esc(item.label)}
${pct}%
`;
});
this.innerHTML = `
${legend.join("")}
`;
}
}
customElements.define("donut-chart", DonutChart);