Pie charts from data
Slices are Arc marks (a new core primitive) around a shared center; DistinctColors gives each a color and SameColor copies it to the legend swatch - no colors in the markup. Set donut>0 for a ring
import { PieChart } from "./PieChart";
// The same component renders any breakdown - only the data changes.
export function PieChartExample() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem" }}>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>Language use</h3>
<PieChart
data={[
{ label: "TypeScript", value: 62 },
{ label: "Rust", value: 18 },
{ label: "Python", value: 12 },
{ label: "Shell", value: 8 },
]}
/>
</div>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>Traffic source (donut)</h3>
<PieChart
donut={0.55}
data={[
{ label: "Direct", value: 40 },
{ label: "Search", value: 35 },
{ label: "Social", value: 25 },
]}
/>
</div>
</div>
);
}
Language use
Traffic source (donut)
How the PieChart component works
Cumulative angles per slice drive Arc start/end; the pie and legend are laid out with distribute + align, and the colors come entirely from constraints
import { Graphic } from "@modular-svg/react";
import { useState } from "react";
export type Slice = { label: string; value: number };
export type PieChartProps = {
data: Slice[];
radius?: number;
/** inner radius as a fraction of radius (>0 makes a donut) */
donut?: number;
fontSize?: number;
};
const sliceId = (label: string) => `slice-${label}`;
const swatchId = (label: string) => `swatch-${label}`;
const rowLabelId = (label: string) => `leglabel-${label}`;
// The legend swatches are the palette: DistinctColors gives each a base
// color, and every slice derives its color from its swatch - SameColor
// normally, or Darken/Lighten while hovering, so the hovered slice pops and
// the rest recede. Hover works from either the slice or the legend.
export function PieChart({
data,
radius = 90,
donut = 0,
fontSize = 13,
}: PieChartProps) {
const [hovered, setHovered] = useState<string | null>(null);
const total = data.reduce((s, d) => s + d.value, 0);
let acc = 0;
const slices = data.map((d) => {
const start = (acc / total) * 360;
acc += d.value;
const end = (acc / total) * 360;
return { ...d, start, end, pct: Math.round((d.value / total) * 100) };
});
const hoverProps = (label: string) => ({
onMouseEnter: () => setHovered(label),
onMouseLeave: () => setHovered(null),
});
return (
<Graphic
style={{
border: "1px solid #ddd",
borderRadius: "8px",
display: "inline-block",
cursor: "pointer",
}}
margin={14}
>
<group>
<group key="pie">
{slices.map((s) => {
const on = hovered === s.label;
return (
<arc
key={sliceId(s.label)}
r={radius}
innerR={donut * radius}
startAngle={s.start}
endAngle={s.end}
stroke={on ? "#333" : "white"}
stroke-width={on ? 3 : 2}
{...hoverProps(s.label)}
/>
);
})}
</group>
<stackV key="legend" spacing={8} alignment="left">
{slices.map((s) => (
<stackH key={`leg-${s.label}`} spacing={8} alignment="centerY">
<rect
key={swatchId(s.label)}
width={14}
height={14}
{...hoverProps(s.label)}
/>
<text
key={rowLabelId(s.label)}
font-size={fontSize}
font-weight={hovered === s.label ? 700 : 400}
{...hoverProps(s.label)}
>
{`${s.label} - ${s.pct}%`}
</text>
</stackH>
))}
</stackV>
<distribute axis="x" spacing={28}>
<ref target="pie" />
<ref target="legend" />
</distribute>
<align alignment="centerY">
<ref target="pie" />
<ref target="legend" />
</align>
{/* Colors (declared after the nodes they reference). The swatches
hold the base palette; slices derive from them. */}
<distinctColors saturation={0.55} lightness={0.6}>
{slices.map((s) => (
<ref key={s.label} target={swatchId(s.label)} />
))}
</distinctColors>
{slices.map((s) => {
if (hovered === s.label) {
return (
<darken key={`sh-${s.label}`} amount={0.18}>
<ref target={swatchId(s.label)} />
<ref target={sliceId(s.label)} />
</darken>
);
}
if (hovered !== null) {
return (
<lighten key={`sh-${s.label}`} amount={0.18}>
<ref target={swatchId(s.label)} />
<ref target={sliceId(s.label)} />
</lighten>
);
}
return (
<sameColor key={`sh-${s.label}`}>
<ref target={swatchId(s.label)} />
<ref target={sliceId(s.label)} />
</sameColor>
);
})}
</group>
</Graphic>
);
}
Venn diagrams from data
Overlapping translucent circles at computed positions; DistinctColors picks each set's color and fill-opacity lets the intersections show through
import { VennDiagram } from "./VennDiagram";
export function VennDiagramExample() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem" }}>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>Two sets</h3>
<VennDiagram sets={[{ label: "Frontend" }, { label: "Backend" }]} />
</div>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>Three sets</h3>
<VennDiagram
sets={[{ label: "HTML" }, { label: "CSS" }, { label: "JS" }]}
/>
</div>
</div>
);
}
Two sets
Three sets
How the VennDiagram component works
Two- or three-set layouts place circles at computed centers with labels along the outward direction
import { Graphic } from "@modular-svg/react";
export type VennSet = { label: string };
export type VennDiagramProps = {
/** two or three sets */
sets: VennSet[];
radius?: number;
fontSize?: number;
};
const circleId = (label: string) => `set-${label}`;
// Overlapping translucent circles. Positions are computed (a Venn is
// inherently coordinate-based), colors come from DistinctColors, and the
// translucency lets the intersections show through.
export function VennDiagram({
sets,
radius = 75,
fontSize = 15,
}: VennDiagramProps) {
const two = sets.length === 2;
// centers and the outward direction for each set's label
const layout = two
? [
{ cx: radius * 0.85, cy: radius, dir: [-1, 0] },
{ cx: radius * 1.65, cy: radius, dir: [1, 0] },
]
: [
{ cx: radius * 1.25, cy: radius * 0.85, dir: [0, -1] },
{ cx: radius * 0.85, cy: radius * 1.55, dir: [-0.87, 0.5] },
{ cx: radius * 1.65, cy: radius * 1.55, dir: [0.87, 0.5] },
];
const placed = sets.slice(0, layout.length).map((set, i) => {
const { cx, cy, dir } = layout[i];
const lx = cx + dir[0] * (radius + 14);
const ly = cy + dir[1] * (radius + 14);
// text x/y is top-left; center the label on its point
const w = set.label.length * fontSize * 0.5;
return { ...set, cx, cy, tx: lx - w / 2, ty: ly - fontSize / 2 };
});
return (
<Graphic
style={{
border: "1px solid #ddd",
borderRadius: "8px",
display: "inline-block",
}}
margin={16}
>
<group>
{placed.map((p) => (
<circle
key={circleId(p.label)}
cx={p.cx}
cy={p.cy}
r={radius}
stroke="#555"
stroke-width={1.5}
fill-opacity={0.45}
/>
))}
{placed.map((p) => (
<text
key={`label-${p.label}`}
x={p.tx}
y={p.ty}
font-size={fontSize}
fill="#222"
>
{p.label}
</text>
))}
<distinctColors saturation={0.6} lightness={0.55}>
{placed.map((p) => (
<ref key={p.label} target={circleId(p.label)} />
))}
</distinctColors>
</group>
</Graphic>
);
}