Donut
Proportional donut / ring chart from summed segment values. Pure geometry + colour props; center is free for a label.
import * as React from "react";
export interface DonutSegment {
value: number;
color: string;
}
export interface DonutProps {
segments: DonutSegment[];
/** px size of the square SVG. */
size?: number;
/** ring thickness in viewBox units (viewBox is 120). */
thickness?: number;
trackColor?: string;
className?: string;
children?: React.ReactNode;
}
/**
* Generic donut/ring chart. Segments are drawn proportionally to their summed value.
* Pure geometry + colour props — design-agnostic.
*/
export function Donut({
segments,
size = 106,
thickness = 15,
trackColor = "rgba(255,255,255,0.06)",
className,
}: DonutProps) {
const r = 60 - thickness / 2 - 2;
const C = 2 * Math.PI * r;
const total = segments.reduce((s, x) => s + Math.max(0, x.value), 0) || 1;
let offset = 0;
return (
<svg className={className} width={size} height={size} viewBox="0 0 120 120" style={{ flex: "0 0 auto" }}>
<circle cx="60" cy="60" r={r} fill="none" stroke={trackColor} strokeWidth={thickness} />
{segments.map((seg, i) => {
const frac = Math.max(0, seg.value) / total;
const len = frac * C;
const el = (
<circle
key={i}
cx="60"
cy="60"
r={r}
fill="none"
stroke={seg.color}
strokeWidth={thickness}
strokeDasharray={`${len} ${C - len}`}
strokeDashoffset={-offset}
transform="rotate(-90 60 60)"
/>
);
offset += len;
return el;
})}
</svg>
);
}