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