| 1 | import { 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(--…)`. */ |
| 4 | export 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 | } |