chartAreaStackedExpand
A Charts block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call chartAreaStackedExpand() with no arguments for a working demo, or edit the code below live.
Implementation notes
@domphy/chart has no native percent/offset stacking mode (verified against engine.ts's accumStackedLines, which only sums raw values). Approximated by pre-normalizing each point to its percentage share before handing data to the engine and locking yAxis to a fixed 0-100 domain, so the stack always fills the plot. Tooltip is wired via a custom valueLabel callback to show the true raw counts (looked up by dataIndex/seriesIndex) even though the plotted heights are normalized shares, per the spec's behavior note. This is a genuine functional gap in the underlying chart engine, not a stub — the visual and tooltip behavior both work as specified via this workaround. Also carries the same mount-reveal approximation caveat as chartAreaDefault.
Status: partial · Reference: shadcn/ui original
// shadcn/ui "chart-area" (stacked-expand recipe) — clean-room reimplementation.
//
// A three-series stacked area chart normalized to a 0–100% share of total at
// every x position, so the combined height always fills the plot — turning
// absolute values into a proportion-of-total ribbon chart.
//
// FIDELITY NOTE: @domphy/chart's `stack` mechanism (see
// packages/chart/src/engine.ts accumStackedLines) only sums raw values into a
// cumulative baseline — there is no native "percent"/offset stacking mode
// (ECharts' `stack: "..."` + a percent axis type has no equivalent surfaced
// on LineSeriesOption/AxisOption here). This recipe approximates it by
// PRE-NORMALIZING each point to its percentage share before handing the data
// to the engine, then locking the y-axis to a fixed 0–100 domain so the
// stacked total is always flush with the plot's top edge. The tooltip is
// wired to show the underlying raw counts (via chartAxisTooltipFormatter's
// custom valueLabel) even though the plotted heights are the normalized
// shares, per the spec's behavior note.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.
import type { DomphyElement } from "@domphy/core";
import type { ChartOption, TooltipParams } from "@domphy/chart";
import type { ThemeColor } from "@domphy/theme";
import {
CHART_AREA_SERIES_PALETTE,
CHART_AREA_THREE_SERIES_DATA,
CHART_AREA_X_AXIS_BARE,
chartAreaFrame,
chartAxisTooltipFormatter,
chartCardShell,
chartTrendFooter,
type ChartAreaThreeSeriesPoint,
type ChartTrendDirection,
} from "./chart-area-shared.js";
export interface ChartAreaStackedExpandSeries {
key: "desktop" | "mobile" | "other";
label: string;
color: ThemeColor;
opacity?: number;
}
export interface ChartAreaStackedExpandProps {
data?: ChartAreaThreeSeriesPoint[];
series?: ChartAreaStackedExpandSeries[];
title?: string;
description?: string;
trendText?: string;
trendDirection?: ChartTrendDirection;
captionText?: string;
height?: number;
}
const DEFAULT_SERIES: ChartAreaStackedExpandSeries[] = [
{ key: "desktop", label: "Desktop", color: CHART_AREA_SERIES_PALETTE[0], opacity: 0.4 },
{ key: "mobile", label: "Mobile", color: CHART_AREA_SERIES_PALETTE[1], opacity: 0.4 },
// Minor category recedes visually at a lower opacity, per spec.
{ key: "other", label: "Other", color: CHART_AREA_SERIES_PALETTE[2], opacity: 0.1 },
];
/**
* shadcn/ui "chart-area" stacked-expand recipe — three category series
* normalized to a percent-of-total stack. Call with no arguments for a
* working demo.
*/
function chartAreaStackedExpand(props: ChartAreaStackedExpandProps = {}): DomphyElement<"div"> {
const {
data = CHART_AREA_THREE_SERIES_DATA,
series = DEFAULT_SERIES,
title = "Area Chart - Stacked Expand",
description = "Showing traffic share by device for the last 6 months",
trendText = "Trending up by 5.2% this month",
trendDirection = "up",
captionText = `${data[0]?.month ?? ""} - ${data[data.length - 1]?.month ?? ""} 2026`,
height = 64,
} = props;
const categories = data.map((point) => point.month);
// Raw counts per category, in series order — used to reconstitute the true
// values in the tooltip since the plotted `data` below is normalized.
const rawByIndex: number[][] = data.map((point) => series.map((s) => point[s.key]));
const totalsByIndex = rawByIndex.map((row) => row.reduce((sum, value) => sum + value, 0) || 1);
const percentData = series.map((s, seriesIndex) =>
data.map((point, dataIndex) => (point[s.key] / totalsByIndex[dataIndex]) * 100),
);
const valueLabel = (p: TooltipParams) => {
const raw = rawByIndex[p.dataIndex]?.[p.seriesIndex];
return raw === undefined ? String(p.value ?? "") : String(raw);
};
const option: ChartOption = {
tooltip: {
trigger: "axis",
formatter: chartAxisTooltipFormatter(categories, valueLabel),
},
xAxis: { ...CHART_AREA_X_AXIS_BARE, data: categories },
// Fixed 0–100 domain, hidden — the stack always fills the plot exactly.
yAxis: { type: "value", min: 0, max: 100, show: false },
grid: { left: 8, right: 8, top: 12, bottom: 24, containLabel: false },
series: series.map((s, seriesIndex) => ({
type: "line",
name: s.label,
stack: "share",
smooth: true,
showSymbol: false,
color: s.color,
lineStyle: { width: 2 },
areaStyle: { opacity: s.opacity ?? 0.4 },
data: percentData[seriesIndex],
})),
};
return chartCardShell({
title,
description,
content: { div: [chartAreaFrame(option, height)] },
footer: chartTrendFooter({ trendText, direction: trendDirection, captionText }),
});
}
export { chartAreaStackedExpand };