1import { For } from "solid-js";
2
3/** Part-to-whole ring; slices are separated by a surface-colored gap rather than a stroke. Colors may be `var(--…)`. */
4export function Donut(props: {
5 slices: { label: string; value: number; color: string }[];
6 format: (value: number) => string;
7 center: string;
8 caption: string;
9}) {
10 const R = 52;
11 const C = 2 * Math.PI * R;
12 const arcs = () => {
13 const total = props.slices.reduce((sum, slice) => sum + slice.value, 0) || 1;
14 let offset = 0;
15 return props.slices.map((slice) => {
16 const length = (slice.value / total) * C;
17 const arc = { ...slice, dash: `${Math.max(0, length - 2)} ${C}`, offset: -offset };
18 offset += length;
19 return arc;
20 });
21 };
22 return (
23 <div class="donut-row">
24 <svg width="140" height="140" viewBox="0 0 140 140" role="img" aria-label={props.caption}>
25 <g transform="rotate(-90 70 70)">
26 <For each={arcs()}>
27 {(arc) => (
28 <circle cx="70" cy="70" r={R} fill="none" style={{ stroke: arc.color }} stroke-width="16"
29 stroke-dasharray={arc.dash} stroke-dashoffset={arc.offset} data-tip={`${arc.label}: ${props.format(arc.value)}`} />
30 )}
31 </For>
32 </g>
33 <text x="70" y="68" text-anchor="middle" fill="var(--text)" font-size="17" font-weight="650">{props.center}</text>
34 <text x="70" y="86" text-anchor="middle" fill="var(--muted)" font-size="11">{props.caption}</text>
35 </svg>
36 <div class="legend">
37 <For each={props.slices}>
38 {(slice) => (
39 <div class="legend-item">
40 <span class="swatch" style={{ background: slice.color }} />
41 <span>{slice.label}</span>
42 <span class="v">{props.format(slice.value)}</span>
43 </div>
44 )}
45 </For>
46 </div>
47 </div>
48 );
49}