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);