Skip to content
Domphy

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.

Props

PropTypeDescription
dataChartAreaThreeSeriesPoint[]
seriesChartAreaStackedExpandSeries[]
titlestring
descriptionstring
trendTextstring
trendDirectionChartTrendDirection
captionTextstring
heightnumber
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. VISUAL QA FIX (2026-07-04): separately, a real @domphy/chart engine bug made this render as a single pale block with only the top series' line visible — LineRenderer's area fill always used the value-axis zero line as its bottom edge instead of the previous stacked series' cumulative curve, so each later series in the stack fully painted over the ones beneath it (gl/LineRenderer.ts). Fixed at the engine layer: accumStackedLines (engine.ts) now also returns each series' pre-stack running-total baseline, and LineRenderer draws the area as a band between that baseline and its own curve, matching gl/BarRenderer.ts's existing stacked-bar behavior. No recipe-level change needed. Also carries the same mount-reveal approximation caveat as chartAreaDefault. Direct-source-diff fix (2026-07-05): Horizontal gridlines were missing on its inline 0-100 y-axis. Fixed.

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 { ChartOption, TooltipParams } from "@domphy/chart";
import type { DomphyElement } from "@domphy/core";
import {
  CHART_AREA_THREE_SERIES_DATA,
  CHART_AREA_X_AXIS_BARE,
  type ChartAreaSeriesTone,
  type ChartAreaThreeSeriesPoint,
  type ChartTrendDirection,
  chartAreaFrame,
  chartAreaGradientFill,
  chartAreaSeriesColor,
  chartAxisTooltipFormatter,
  chartCardShell,
  chartTrendFooter,
} from "./chart-area-shared.js";

export interface ChartAreaStackedExpandSeries {
  key: "desktop" | "mobile" | "other";
  label: string;
  /** Ramp tone within the primary family (approximates var(--chart-N)). */
  tone: ChartAreaSeriesTone;
  opacity?: number;
}

export interface ChartAreaStackedExpandProps {
  data?: ChartAreaThreeSeriesPoint[];
  series?: ChartAreaStackedExpandSeries[];
  title?: string;
  description?: string;
  trendText?: string;
  trendDirection?: ChartTrendDirection;
  captionText?: string;
  height?: number;
}

// Declared bottom-to-top to match upstream's <Area> order (other → mobile →
// desktop): the engine stacks series[0] at the bottom, so the faint `other`
// band sits at the base and the `desktop` band on top, and the axis-tooltip
// rows list Other, Mobile, Desktop in this same order. Each key takes the
// next step of the single-hue ramp (chart-1 → chart-3).
const DEFAULT_SERIES: ChartAreaStackedExpandSeries[] = [
  // Minor category recedes visually at a lower opacity, per spec.
  {
    key: "other",
    label: "Other",
    tone: chartAreaSeriesColor(0).tone,
    opacity: 0.1,
  },
  {
    key: "mobile",
    label: "Mobile",
    tone: chartAreaSeriesColor(1).tone,
    opacity: 0.4,
  },
  {
    key: "desktop",
    label: "Desktop",
    tone: chartAreaSeriesColor(2).tone,
    opacity: 0.4,
  },
];

/**
 * 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",
      axisPointer: { type: "none" },
      // Upstream passes `<ChartTooltipContent indicator="line" />`.
      formatter: chartAxisTooltipFormatter(
        categories,
        valueLabel,
        false,
        "line",
      ),
    },
    xAxis: { ...CHART_AREA_X_AXIS_BARE, data: categories },
    // Fixed 0–100 domain — the stack always fills the plot exactly. Chrome is
    // hidden but the horizontal split gridlines stay on, mirroring upstream's
    // `<CartesianGrid vertical={false} />`.
    yAxis: {
      type: "value",
      min: 0,
      max: 100,
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: { show: false },
      splitLine: { show: true },
    },
    grid: { left: 8, right: 8, top: 12, bottom: 32, containLabel: false },
    series: series.map((s, seriesIndex) => ({
      type: "line",
      name: s.label,
      stack: "share",
      smooth: true,
      showSymbol: false,
      // The engine pins strokes to the family at shift-9; the ramp step is
      // approximated via stroke opacity, and the fill carries the exact tone.
      color: "primary",
      lineStyle: {
        width: 2,
        opacity: chartAreaSeriesColor(seriesIndex).strokeOpacity,
      },
      areaStyle: {
        color: chartAreaGradientFill(
          "primary",
          s.opacity ?? 0.4,
          s.opacity ?? 0.4,
          s.tone,
        ),
        opacity: 1,
      },
      data: percentData[seriesIndex],
    })),
  };

  return chartCardShell({
    title,
    description,
    content: { div: [chartAreaFrame(option, height)] },
    footer: chartTrendFooter({
      trendText,
      direction: trendDirection,
      captionText,
    }),
  });
}

export { chartAreaStackedExpand };

← Back to shadcn/ui catalog