chartPieStacked
A Charts block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call chartPieStacked() with no arguments for a working demo, or edit the code below live.
Implementation notes
Two concentric rings (inner 30->56, outer 56->86 viewBox units, contiguous with no gap band) share the same category order and index-based color mapping so both rings visibly agree per category; each ring's own metric normalizes its own 360 degrees independently (own judgment — the spec did not require synchronized angles across rings, only radial continuity). Tooltip uses a thin line marker instead of a filled swatch and prefixes the category with the ring's own series label (e.g. 'Sessions'), per spec. Same mount-sweep approximation caveat as chartPieSimple.
Status: ported · Reference: shadcn/ui original
// shadcn/ui "charts/pie-stacked" — clean-room reimplementation.
//
// Two concentric donut rings in one plot: a smaller inner ring for one
// metric and a larger outer ring for a second metric, both sharing the same
// category keys and the same color-per-category mapping (the inner ring's
// outer radius sits exactly at the outer ring's inner radius, so together
// they read as one continuous two-layer ring). Implemented purely from the
// block's public functional/visual spec — no upstream source was viewed.
import type { DomphyElement } from "@domphy/core";
import { motion } from "@domphy/ui";
import type { ThemeColor } from "@domphy/theme";
import {
PIE_OUTER_RADIUS,
createPieTooltipState,
defaultValueFormatter,
layoutPieSlices,
pieCard,
pieCardDescription,
pieCardFooter,
pieCardTitle,
pieChartContainer,
pieWedgePath,
type PieDatum,
} from "./pie-chart-shared.js";
export interface PieStackedDatum {
key: string;
name: string;
inner: number;
outer: number;
color?: ThemeColor;
}
// Illustrative sample data (two metrics per month) — not a required schema.
export const DEFAULT_STACKED_DATA: PieStackedDatum[] = [
{ key: "jan", name: "January", inner: 186, outer: 80 },
{ key: "feb", name: "February", inner: 305, outer: 200 },
{ key: "mar", name: "March", inner: 237, outer: 120 },
{ key: "apr", name: "April", inner: 173, outer: 190 },
{ key: "may", name: "May", inner: 209, outer: 130 },
{ key: "jun", name: "June", inner: 214, outer: 140 },
];
const INNER_RING_INNER_RADIUS = 30;
const INNER_RING_OUTER_RADIUS = 56;
// No gap: the outer ring's inner radius picks up exactly where the inner
// ring's outer radius ends, so the two rings read as one banded ring.
const OUTER_RING_INNER_RADIUS = INNER_RING_OUTER_RADIUS;
const OUTER_RING_OUTER_RADIUS = PIE_OUTER_RADIUS;
export interface ChartPieStackedProps {
data?: PieStackedDatum[];
title?: string;
description?: string;
trendValue?: string;
trendDirection?: "up" | "down";
caption?: string;
valueFormatter?: (value: number) => string;
innerSeriesLabel?: string;
outerSeriesLabel?: string;
}
/**
* Two concentric donut rings sharing one categorical color mapping, each
* ring driven by its own metric. Call with no arguments for a fully working
* demo.
*/
function chartPieStacked(props: ChartPieStackedProps = {}): DomphyElement<"div"> {
const {
data = DEFAULT_STACKED_DATA,
title = "Pie Chart - Stacked",
description = "January - June 2024",
trendValue = "5.2%",
trendDirection = "up",
caption = "Showing total visitors for the last 6 months",
valueFormatter = defaultValueFormatter,
innerSeriesLabel = "Sessions",
outerSeriesLabel = "Visitors",
} = props;
const toPieDatum = (metric: "inner" | "outer"): PieDatum[] =>
data.map((record) => ({
key: record.key,
name: record.name,
value: record[metric],
color: record.color,
}));
const innerSlices = layoutPieSlices(toPieDatum("inner"));
const outerSlices = layoutPieSlices(toPieDatum("outer"));
const tooltipState = createPieTooltipState();
const containerRef = { current: null as HTMLElement | null };
const innerWedges: DomphyElement<"path">[] = innerSlices.map((slice) =>
pieWedgePath(slice, {
innerRadius: INNER_RING_INNER_RADIUS,
outerRadius: INNER_RING_OUTER_RADIUS,
keyPrefix: "inner-",
tooltip: {
containerRef,
tooltipState,
valueFormatter,
markerShape: "line",
seriesLabel: innerSeriesLabel,
},
}),
);
// Both rings resolve colors index-by-index (see resolveSliceColor in the
// shared module), and `toPieDatum` preserves category order across both
// arrays, so the two rings visibly agree per category without any extra
// color-sharing plumbing here.
const outerWedges: DomphyElement<"path">[] = outerSlices.map((slice) =>
pieWedgePath(slice, {
innerRadius: OUTER_RING_INNER_RADIUS,
outerRadius: OUTER_RING_OUTER_RADIUS,
keyPrefix: "outer-",
tooltip: {
containerRef,
tooltipState,
valueFormatter,
markerShape: "line",
seriesLabel: outerSeriesLabel,
},
}),
);
return pieCard([
pieCardTitle(title),
pieCardDescription(description),
pieChartContainer(
containerRef,
[
{
g: [...innerWedges, ...outerWedges],
style: { transformOrigin: "100px 100px" },
$: [
motion({
initial: { opacity: 0, scale: 0.7 },
animate: { opacity: 1, scale: 1 },
transition: { duration: 700, easing: "ease-out" },
}),
],
} as DomphyElement<"g">,
],
tooltipState,
),
pieCardFooter({ trendValue, trendDirection, caption }),
]);
}
export { chartPieStacked };