022 / RECHARTS / BAR & COLUMN

Percent Stack Bars

Recharts

100% normalized stacked bars for comparing categorical composition while preserving raw contributor values, stable identity, and truthful zero/missing semantics.

SPEC
#022
ENGINE
Recharts
FAMILY
Bar & Column
RENDERER
svg
STATUS
preview

Installation

PLOTCN/REGISTRY/BAR-PERCENT-STACK/SOURCE
pnpm dlx shadcn@latest add @plotcn/bar-percent-stack

Checking public registry…

View local registry JSON
REGISTRY direct URL·ENGINE Recharts·FILES 1·DEPENDENCIES 5

Copied as source into your project (requires recharts).

× 340Container width
Measuring preview...
RECHARTS · SVG · 0 × 340pxMotion enabled · ResizeObserver

Overview

Percent Stack Bars is Plotcn's canonical 100% normalized stacked bar component designed specifically for comparing categorical composition across discrete groups. It answers the fundamental analytical question:

“How does the composition of the whole differ across categories?”

Secondary analytical questions answered by this component include:

  • “What share of the visible total does each contributor represent?”
  • “How do those proportional shares change from one category to another?”
  • “What raw underlying measurements produced those normalized percentages?”

While PercentStreamArea (013) visualizes continuous composition across an ordered timeline domain, PercentStackBars specializes in discrete, categorical groups (such as customer segments, company divisions, subscription plans, or product cohorts).

TSX
import { PercentStackBars } from "@/components/charts/recharts/bar-percent-stack"const subscriptionMix = [  { segment: "Startup", monthly: 620, annual: 310, multiYear: 70 },  { segment: "Growth", monthly: 840, annual: 920, multiYear: 240 },  { segment: "Enterprise", monthly: 90, annual: 215, multiYear: 195 },]export function SubscriptionCompositionChart() {  return (    <PercentStackBars      data={subscriptionMix}      categoryKey="segment"      series={[        { key: "monthly", label: "Monthly", color: "var(--chart-1)" },        { key: "annual", label: "Annual", color: "var(--chart-2)" },        { key: "multiYear", label: "Multi-year", color: "var(--chart-3)" },      ]}      showLegend      showGrid    />  )}

Governing Principle

Percent Stack Bars intentionally removes total-magnitude differences so that composition can be compared across categories. Every complete visible category normalizes to 100%, contributor identity remains stable, and percentages must always derive from real raw values rather than invented shares.

This principle governs data preprocessing, fixed domain bounds, tooltips, legend toggling, zero totals, missing values, and accessibility alternatives.

Bar-Family Positioning

The Plotcn Bar family provides five distinct analytical specializations:

Component Registry ID Primary Analytical Question Quantitative Scale
ComponentRegistry IDPrimary Analytical QuestionQuantitative Scale
Signal Bars (019)bar-signal"How do discrete categories compare on single or grouped measures?"Grounded Zero (0Max0 \to \text{Max})
Rank Bars (020)bar-rank"Which categories perform highest or lowest in top-N rank?"Grounded Zero (0Max0 \to \text{Max})
Group Compare (021)bar-group-compare"How do peer measures compare directly against each other?"Shared Grounded Zero (0Max0 \to \text{Max})
Stack Ledger (022)bar-stack-ledger"How do additive contributors compose the total magnitude?"Additive Total (0Max Total0 \to \text{Max Total})
Percent Stack (023)bar-percent-stack"How does the internal percentage composition differ across categories?"Fixed Normalized (0%100%0\% \to 100\%)

Absolute Stack vs. Percent Stack

ANALYTICAL DIFFERENCEComponent 022 vs Component 023

Stack Ledger Bars (Absolute) vs Percent Stack Bars (100% Normalized)

Comparison diagram illustrating how Stack Ledger Bars encodes total category magnitude via varying bar heights, while Percent Stack Bars normalizes every category to 100% height to compare composition alone.

  • Stack Ledger Bars (022): Preserves volume differences. If Category A has a total of 100 and Category B has a total of 200, Category B's bar is twice as tall.
  • Percent Stack Bars (023): Discards volume differences. Both Category A and Category B render to exactly 100% of the quantitative axis. The viewer's attention is focused solely on internal distribution differences.

Dedicated Component — Not stackMode="percent"

Plotcn deliberately avoids adding stackMode="percent" to StackLedgerBars. Comparing absolute additive volume and comparing normalized proportional share are two distinct cognitive tasks:

  • Stack Ledger: Answers "How large is the whole, and how much did each contributor add?"
  • Percent Stack: Answers "What proportion of the whole belongs to each contributor?"

Exposing a toggle between these modes in a single component causes subtle bugs: tooltips oscillate between currency and percentages, domain configurations conflict, zero-total categories trigger divide-by-zero crashes, and legend toggles produce ambiguous denominator semantics. PercentStackBars is therefore an independent, specialized component.

Magnitude-Loss Rule

A 100% stacked chart does not communicate how large the category total is.
MAGNITUDE-LOSS RULE100x Volume Difference → Identical Geometry

The Magnitude-Loss Principle: Equal Visual Heights Discard Total Scale

Explanatory diagram showing Category A with raw total 100 and Category B with raw total 10,000 having identical 50/30/20 normalized visual stacks, proving that Percent Stack Bars conveys composition rather than absolute volume.

Consider two customer tiers:

  • Startup Tier: 50 Monthly, 30 Annual, 20 Enterprise. Raw Total = 100 users.
  • Global Enterprise Tier: 5,000 Monthly, 3,000 Annual, 2,000 Enterprise. Raw Total = 10,000 users.

In PercentStackBars, both tiers produce identical normalized bars: 50% / 30% / 20%. The 100× difference in total volume is intentionally eliminated from the bar geometry. If absolute magnitude is required alongside composition, use StackLedgerBars or present the total in surrounding dashboard UI.

Normalization Pipeline

Raw measurements are transformed into 100% normalized geometry through a deterministic 7-step pipeline:

NORMALIZATION PIPELINERaw Additive → 100% Geometry

Deterministic Normalization Pipeline: From Raw Values to 100% Shares

Data flow diagram showing input records validating non-negative numbers, filtering visible series, computing the visible raw total, deriving exact proportional shares, and rendering a normalized 100% stack.

  1. Consumer Raw Data: Caller supplies raw additive non-negative numbers. Caller arrays remain strictly immutable.
  2. Contract & Color Resolution: Contributor colors map directly from configured series indices (--chart-1, --chart-2, etc.) and remain fixed.
  3. Non-Negative Validation: Any negative value (V<0V < 0) halts normalization and triggers a truthful error state.
  4. Visibility Filtering: Active legend toggles determine which contributors participate in the denominator.
  5. Denominator Calculation: Visible contributor raw values are summed to calculate the visible raw total.
  6. Proportional Allocation: For positive totals (S>0S > 0), each contributor receives (Vi/S)×100(V_i / S) \times 100.
  7. Fixed 0–100 Stacking: Recharts renders normalized values on a fixed quantitative axis with ticks at 0%, 25%, 50%, 75%, and 100%.

Raw Values & Derived Shares

Percentages are always derived from canonical raw numbers, never ingested as source data:

DUAL-TIER DATACanonical Truth → Derived Representation

Raw Measurement vs Derived Proportional Share

Architecture diagram showing that caller raw measurements are preserved as canonical source data in tooltips and accessibility, while normalized percentages are derived at runtime for SVG bar heights.

  • Display Tooltip: Concurrently exposes both the derived share (e.g. 60.0%) and the raw count (e.g. 620 users), concluding with the visible category total (1,000 users).
  • Precision Guard: Geometry derives from full-precision floating-point ratios so adjacent segments join seamlessly without rounding gaps. Rounding is applied only during label formatting.

Zero-Total Semantics: Zero Is Not Equal Shares

What happens when every contributor in a category is zero (0+0+0=00 + 0 + 0 = 0)?

TRUTHFUL ZERO TOTALNo Fabricated Equal Shares

Zero-Total Category Semantics: Undefined Share vs False Invention

Comparison showing correct Plotcn behavior when all contributors are zero (0+0+0): the composition is flagged unavailable and no bar is drawn, preventing the visual deception of inventing 33.3% equal shares.

Plotcn never invents equal shares for a zero-total category.

Three zeros do not mean that each contributor owns 33.3%. Dividing zero by zero (0/00 / 0) is mathematically undefined.

Under PercentStackBars:

  • Category band is preserved along the categorical axis.
  • No normalized bar geometry is rendered.
  • Tooltip factually states: "Total measurement is zero. Proportional share is undefined."
  • Screen reader announces: "Raw total 0. Percentage composition unavailable because the visible total is zero."
  • Offscreen table registers Zero Total.

Missing vs. Zero vs. Hidden

THREE DISTINCT STATESZero ≠ Missing ≠ Hidden

Categorical State Disambiguation: Zero, Missing, and Hidden Contributor

State matrix diagram showing Zero as a known 0 contribution, Missing as an unavailable measurement causing incomplete composition by default, and Hidden as a user-initiated exclusion that renormalizes the visible total.

Plotcn enforces strict distinctions between three easily confused states:

1. Zero Value (V=0V = 0)

  • Known, recorded measurement.
  • Contributes 0 to raw total.
  • Normalized share is 0.0%.
  • Segment has zero visual thickness but remains listed in tooltips and legends.

2. Missing Value (null / undefined / NaN)

  • Unrecorded or unavailable measurement.
  • Default Policy ("incomplete"): Without all contributors, the true denominator cannot be known. Stack geometry is suppressed and the category is marked incomplete.
  • Opt-in Policy ("zero"): When missingValuePolicy="zero" is explicitly set, missing values are coerced to 0 before normalization.

3. Hidden Contributor (User Legend Toggle)

  • Known measurement intentionally excluded by the user.
  • Excludes contributor from the denominator.
  • Remaining visible contributors renormalize to 100%.

Legend Renormalization: Visibility Changes the Denominator

When an interactive legend hides a series, PercentStackBars renormalizes all remaining visible series to 100%:

DENOMINATOR SHIFTVisibility Changes the Denominator

Legend Renormalization: Hiding Series Recalculates Proportions to 100%

Step diagram showing Category with A=50, B=30, C=20 totaling 100 with shares 50%, 30%, 20%. When C is hidden, the visible total becomes 80 and the remaining contributors renormalize to 62.5% and 37.5%, keeping the stack at 100%.

Suppose a category contains:

  • Series A: 50 (50%)
  • Series B: 30 (30%)
  • Series C: 20 (20%)

When Series C is toggled off:

  • Visible Raw Total changes from 10080100 \to 80.
  • Series A becomes 50/80=62.5%50 / 80 = \mathbf{62.5\%}.
  • Series B becomes 30/80=37.5%30 / 80 = \mathbf{37.5\%}.
  • The visible stack continues to occupy exactly 100% of the bar extent.
  • Series colors never shift. Series A remains --chart-1 and Series B remains --chart-2.

If all series are hidden, the chart enters a polite, recoverable empty state with a "Show all contributors" action.

Stable Stack Order

SPATIAL COGNITIONConsistent Layer Identity

Stable Stack Order: Canonical Series Hierarchy vs Harmful Dynamic Sorting

Comparison showing why Plotcn maintains the same series stacking order across all categories (Series A on bottom, B in middle, C on top) instead of sorting dynamically by share, which causes eye-tracking confusion.

The series configuration array establishes a permanent vertical hierarchy:

TEXT
series[0] (Bottom)  →  series[1] (Middle)  →  series[2] (Top)

Plotcn never dynamically reorders segments by share percentage. Dynamic reordering forces viewers to re-scan legends on every category, destroying horizontal eye-tracking and creating visual chaos.

Dual-Tier Interaction Architecture & Hit Testing

INTERACTION ARCHITECTUREForgiving Category Band + Exact Segment Target

Dual-Tier Hit Testing: Broad Category Band with Tiny-Segment Protection

Hit region diagram showing full 44px vertical category strip that activates the tooltip upon hover, protecting tiny segments from being untargetable on touch or pointer devices.

Inspecting tiny percentage segments (e.g. a 1.5% enterprise share) can be challenging on touchscreens or with imprecise mouse pointers. Plotcn provides two-tier hit testing:

  1. Broad Category Band: Hovering anywhere within the category column triggers the unified category tooltip, listing all contributors, shares, and totals.
  2. Individual Segment Hover: Moving directly over a specific segment highlights that series row within the tooltip and emphasizes the corresponding legend item.
  3. No Artificial Inflation: Plotcn never artificially inflates small segments to a minimum pixel height, preserving mathematical truthfulness.

Vertical vs. Horizontal Orientation

LAYOUT FLEXIBILITYVertical (Columns) vs Horizontal (Rows)

Vertical vs Horizontal Orientation: First-Class Label Readability

Layout diagram illustrating vertical layout suitable for concise categories along X, versus horizontal layout where bars grow rightward along Y to accommodate long enterprise labels without clipping.

  • Vertical (layout="vertical", default): Categories along X, stacks rise vertically to 100%. Ideal for concise category labels, quarters, and dates.
  • Horizontal (layout="horizontal"): Categories along Y, stacks extend horizontally to 100%. Ideal for long descriptive strings, department names, or localized labels without diagonal truncation.

Color Customization & Theme Tokens

Series colors default to Plotcn's adaptive theme tokens (var(--chart-1) through var(--chart-8)). Individual series can specify custom colors or theme tokens:

TSX
<PercentStackBars  data={data}  categoryKey="segment"  series={[    { key: "monthly", label: "Monthly", color: "var(--chart-1)" },    { key: "annual", label: "Annual", color: "var(--chart-2)" },    { key: "multiYear", label: "Multi-year", color: "#8b5cf6" },  ]}/>

Custom color overrides do not trigger normalization recomputations. Adjacent segments remain distinguishable through 1px structural boundaries (stroke="var(--background)").

03 / Component API & Styling

Props Reference & Interactive Prop Explorer

Inspect every component property, customize semantic color roles live with instant visual feedback, and copy production-ready code with active prop configurations.

Colors & Appearance Configuration

Customize primary, reference, or annotation series colors. Defaults derive from Plotcn theme tokens.

Generated Usage Code (Live Props):
<PercentStackBars
  data={data}
  categoryKey="segment"
  series={[
    { key: "monthly", label: "Monthly" },
    { key: "annual", label: "Annual" },
    { key: "multiYear", label: "Multi-year" },
  ]}
/>
Interactive Prop Preview Lab
layout"vertical" | "horizontal"

Orientation layout: "vertical" (categories on X, stacks grow upward to 100%) or "horizontal" (categories on Y, stacks grow rightward).

Select value to preview live:
Active: layout="vertical"Default: "vertical"
groupGapnumber

Space between adjacent category stacks along the categorical axis in pixels.

Select value to preview live:
Active: groupGap={20}Default: 20
maxBarSizenumber

Maximum width/thickness for individual stacked bars in pixels.

Select value to preview live:
Active: maxBarSize={48}Default: 48
showGridboolean

Whether to render subtle reference grid lines at 0%, 25%, 50%, 75%, 100%.

Select value to preview live:
Active: showGrid={true}Default: true
showLegendboolean

Whether to render the series identity legend.

Select value to preview live:
Active: showLegend={true}Default: true
interactiveLegendboolean

Whether clicking legend items toggles series visibility. Hiding contributors dynamically renormalizes remaining visible series to 100%.

Select value to preview live:
Active: interactiveLegend={true}Default: true
valueLabel"none" | "auto"

Value label rendering mode: "none" or "auto" (render percentage labels inside segments when space permits).

Select value to preview live:
Active: valueLabel="none"Default: "none"
missingValuePolicy"incomplete" | "zero"

Handling of missing values: "incomplete" marks composition unavailable and suppresses stack geometry; "zero" coerces missing values to 0.

Select value to preview live:
Active: missingValuePolicy="incomplete"Default: "incomplete"
All Properties (19)
Component properties
PropertyTypeDefaultRequiredDescription
dataReq
readonly TData[][]Yes

Readonly array of categorical records. Caller data is immutable and never modified.

keyof TData & stringYes

Property key on data records representing the discrete category label.

readonly PercentStackBarSeries<TData>[][]Yes

Array of two or more additive series definitions composing the category whole.

"vertical" | "horizontal""vertical"No

Orientation layout: "vertical" (categories on X, stacks grow upward to 100%) or "horizontal" (categories on Y, stacks grow rightward).

number | string340No

Chart container height in pixels or CSS dimension string.

number20No

Space between adjacent category stacks along the categorical axis in pixels.

number48No

Maximum width/thickness for individual stacked bars in pixels.

booleantrueNo

Whether to render subtle reference grid lines at 0%, 25%, 50%, 75%, 100%.

booleantrueNo

Whether to render the categorical axis (vertical) or percentage scale (horizontal).

booleantrueNo

Whether to render the percentage scale (vertical) or categorical axis (horizontal).

booleantrueNo

Whether to render the series identity legend.

booleantrueNo

Whether clicking legend items toggles series visibility. Hiding contributors dynamically renormalizes remaining visible series to 100%.

"none" | "auto""none"No

Value label rendering mode: "none" or "auto" (render percentage labels inside segments when space permits).

"incomplete" | "zero""incomplete"No

Handling of missing values: "incomplete" marks composition unavailable and suppresses stack geometry; "zero" coerces missing values to 0.

boolean | { duration?: number }trueNo

Animation configuration. Automatically disabled under prefers-reduced-motion.

(value: number) => stringval => `${val.toFixed(1)}%`No

Custom formatter function for normalized share percentages.

(category: string | number) => stringString(category)No

Custom formatter function for categorical axis labels.

(value: number) => stringvalue.toLocaleString()No

Global fallback formatter function for raw numeric values in tooltips.

(active: ActivePercentStackDatum<TData> | null) => voidNo

Callback fired when the active inspected category changes.

04 / Cookbook & States

Component Variants & Edge States

Production cookbooks showcasing configuration variants alongside verified handling of loading, empty data, and network error states.

Subscription Mix by Segment

100% normalized stacked bars comparing subscription plan distribution across customer segments with uneven raw totals.

<PercentStackBars
  data={[
    { segment: "Startup", monthly: 620, annual: 310, multiYear: 70 },
    { segment: "Growth", monthly: 840, annual: 920, multiYear: 240 },
    { segment: "Enterprise", monthly: 90, annual: 215, multiYear: 195 },
  ]}
  categoryKey="segment"
  series={[
    { key: "monthly", label: "Monthly", color: "var(--chart-1)" },
    { key: "annual", label: "Annual", color: "var(--chart-2)" },
    { key: "multiYear", label: "Multi-year", color: "var(--chart-3)" },
  ]}
/>

Same Composition Across Unequal Totals

Demonstrating the magnitude-loss rule: two categories with identical 50/30/20 proportions render identical 100% bar geometry despite a 100x difference in raw volume.

<PercentStackBars
  data={[
    { segment: "Small Cohort (Total: 100)", monthly: 50, annual: 30, multiYear: 20 },
    { segment: "Large Cohort (Total: 10,000)", monthly: 5000, annual: 3000, multiYear: 2000 },
  ]}
  categoryKey="segment"
  series={[
    { key: "monthly", label: "Monthly", color: "var(--chart-1)" },
    { key: "annual", label: "Annual", color: "var(--chart-2)" },
    { key: "multiYear", label: "Multi-year", color: "var(--chart-3)" },
  ]}
/>

Horizontal Enterprise Layout

Explicit horizontal orientation recommended for lengthy category labels, keeping text unclipped and readable.

<PercentStackBars
  data={[
    { department: "Enterprise Customer Success", selfService: 28, assisted: 44, dedicated: 28 },
    { department: "Government & Public Sector", selfService: 12, assisted: 36, dedicated: 52 },
    { department: "International Operations", selfService: 40, assisted: 35, dedicated: 25 },
  ]}
  categoryKey="department"
  layout="horizontal"
  height={280}
  series={[
    { key: "selfService", label: "Self-Service", color: "var(--chart-1)" },
    { key: "assisted", label: "Assisted", color: "var(--chart-2)" },
    { key: "dedicated", label: "Dedicated", color: "var(--chart-3)" },
  ]}
/>

Missing Contributor Incomplete State

Demonstrating how missing data in a category invalidates composition by default to prevent visual deception.

<PercentStackBars
  data={[
    { quarter: "Q1", web: 520, mobile: 340, partner: 140 },
    { quarter: "Q2", web: 610, mobile: null, partner: 190 },
    { quarter: "Q3", web: 580, mobile: 390, partner: 230 },
  ]}
  categoryKey="quarter"
  missingValuePolicy="incomplete"
  series={[
    { key: "web", label: "Web", color: "var(--chart-1)" },
    { key: "mobile", label: "Mobile", color: "var(--chart-2)" },
    { key: "partner", label: "Partner", color: "var(--chart-3)" },
  ]}
/>

Zero-Total Category Safety

A category where all visible contributors are zero (0+0+0): Plotcn flags the composition unavailable and never invents equal shares.

<PercentStackBars
  data={[
    { tier: "Free Tier", direct: 450, organic: 350, referral: 200 },
    { tier: "Decommissioned Tier", direct: 0, organic: 0, referral: 0 },
    { tier: "Paid Pro Tier", direct: 280, organic: 520, referral: 200 },
  ]}
  categoryKey="tier"
  series={[
    { key: "direct", label: "Direct", color: "var(--chart-1)" },
    { key: "organic", label: "Organic", color: "var(--chart-2)" },
    { key: "referral", label: "Referral", color: "var(--chart-3)" },
  ]}
/>
Lifecycle & Exception States
01. Loading State

Skeletons indicate runtime fetch or pending data queries.

02. Empty Data State

Handles empty collections ([]) gracefully without crashing.

03. Error State

Graceful failure banner when data source or script fails.

05 / Responsive Lab

Container-Driven Breakpoints

Percent Stack Bars maintains full 100% normalized proportional composition across all screen dimensions down to 320px. Category labels thin adaptively, while all configured series remain visible and truthful without silent removal.

Desktop
>= 1024px

Full category tick density, inline percentage labels where configured, and comprehensive synchronized tooltips.

Tablet
640px - 1023px

Adaptive category tick thinning, compact margins, and preserved 44px touch interaction targets.

Mobile
< 640px

Strict vertical page scrolling (pan-y), legend wrapped into compact rows, and automatic hiding of inline segment labels to prevent clutter.

Tablet Preview (768px Container Constraint)
Mobile Preview (390px Container Constraint)
06 / Assistive Technology

Accessibility & Navigation Standards

Percent Stack Bars provides dual-tier accessibility: an interactive SVG region with a single tab stop and arrow keyboard traversal, accompanied by an offscreen structured HTML table disclosing raw measurements alongside derived proportional shares.

Semantic Role & Landmark

Container mounts as region with explicit assistive label.

Color-Independent Legibility

Consistent canonical stack order, 1px structural segment boundaries, legend labels, and full off-screen HTML data table ensure complete non-color accessibility.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “Polite ARIA live region announces category label, each contributor's derived share and raw value, and visible raw total. Zero-total categories announce composition unavailable because the visible total is zero; incomplete categories announce composition unavailable because the category is incomplete.

Reduced Motion Support

All initial entrance animations are bypassed immediately when prefers-reduced-motion is detected.

Keyboard Interaction Model
Keyboard interaction model
KeyAction
ArrowLeft / ArrowRightTraverses categories in horizontal X order (vertical layout). Updates live region with category, raw measurements, and derived percentages.
ArrowUp / ArrowDownTraverses categories in vertical Y order (horizontal layout).
Home / EndJumps focus directly to the first or last categorical record.
EscapeDismisses active category inspection and clears focus outlines.

Data Safety Checklist

  • [x] Input uses raw additive measurements; percentages are derived internally.
  • [x] Caller data is never mutated; input is treated as strictly readonly.
  • [x] Category order matches caller order; no artificial sorting.
  • [x] Contributor order matches series config order across all categories.
  • [x] Non-negative contract enforced; negative values halt rendering with truthful feedback.
  • [x] Zero-total categories never invent equal shares (0/033.3%0/0 \neq 33.3\%).
  • [x] Missing values render as incomplete by default; treat-as-zero is explicit opt-in.
  • [x] Hiding a series renormalizes remaining visible series to 100%.
  • [x] Hiding a series never shifts color tokens of remaining series.
  • [x] Quantitative domain is fixed at 0–100% with no domain truncation.
  • [x] Tiny shares are never artificially inflated in SVG geometry.

Rendering Architecture

SYSTEM ARCHITECTUREComplete Component Dataflow

PercentStackBars Rendering Architecture: Pipeline, Subsystems & Accessibility

Architecture diagram showing props entering normalization pipeline, state resolution, fixed 0-100 Recharts coordinate system, dual hit-testing, and accessible live region output.

The rendering architecture coordinates pure mathematical normalization with Recharts SVG rendering primitives:

TEXT
PercentStackBars├── Pure Normalization Pipeline (normalizePercentStackData)│   ├── Non-negative validation│   ├── Missing value policy evaluation│   ├── Visible series denominator summation│   └── 0–100% proportional derivation├── Recharts Responsive Container│   ├── Fixed CartesianGrid (0%, 25%, 50%, 75%, 100%)│   ├── Categorical XAxis / YAxis│   ├── Fixed [0, 100] Percentage Axis│   └── Stacked Bar Components (stackId="percent")│       └── Custom Outer Corner Bar Shape (StackedBarShape)├── Interactive Legend (Renormalizing toggles)├── Synchronized Tooltip (Raw values + derived percentages + visible totals)└── Screen Reader ARIA Live Region & Offscreen HTML Table

Props Reference

Property Type Default Required Description
PropertyTypeDefaultRequiredDescription
datareadonly TData[]RequiredReadonly array of categorical records.
categoryKeykeyof TData & stringRequiredField name representing the categorical domain.
seriesreadonly PercentStackBarSeries<TData>[]RequiredAdditive contributor series definitions (key, label, optional color).
layout"vertical" | "horizontal""vertical"OptionalStacking orientation.
heightnumber | string340OptionalContainer height in pixels or CSS dimension string.
groupGapnumber20OptionalSpacing between category bars in pixels.
maxBarSizenumber48OptionalMaximum thickness of stacked bars in pixels.
showGridbooleantrueOptionalWhether to display percentage grid reference lines.
showLegendbooleantrueOptionalWhether to render the series legend.
interactiveLegendbooleantrueOptionalWhether clicking legend items toggles visibility and renormalizes.
valueLabel"none" | "auto""none"OptionalWhether to display inline percentage labels inside segments.
missingValuePolicy"incomplete" | "zero""incomplete"OptionalHandling of missing or non-finite contributor values.
percentageFormatter(value: number) => stringOptionalCustom percentage formatter for tooltips and labels.
categoryFormatter(cat: string | number) => stringOptionalCustom formatter for category axis ticks and tooltips.
motionboolean | { duration?: number }trueOptionalAnimation toggle. Bypassed when prefers-reduced-motion is active.
onActiveChange(active: ActivePercentStackDatum | null) => voidOptionalCallback fired on active category change.

Source Anatomy

The component installs directly into your repository with full source ownership:

  • Component Path: components/charts/recharts/bar-percent-stack.tsx
  • Registry Dependencies: @plotcn/chart-container, @plotcn/chart-state, @plotcn/chart-tooltip, @plotcn/chart-motion
  • NPM Dependencies: recharts
07 / Source Anatomy

Internal Architecture & File Dependencies

Source-first ownership model. Inspect the exact component call tree, dependencies, and full implementation below.

Component Architecture Call Tree
PercentStackBars(Root figure element with keyboard navigation and ARIA accessibility shell)
├──ResponsiveContainer[Responsive container wrapper]

Handles container dimension measurement and SVG viewBox sizing

└──BarChart[Recharts Cartesian SVG coordinator]

Coordinates scales, Cartesian grid, stacked Bar geometry with shared stackId='percent', and Tooltip

Involved Source Files & Registry Assets
registry/recharts/bar-percent-stack.tsx
Complete Percent Stack Bars component for 100% normalized categorical composition with raw values and legend renormalization
registry/recharts/bar-percent-stack.tsx
"use client"import * as React from "react"import {  ResponsiveContainer,  BarChart,  Bar,  XAxis,  YAxis,  Tooltip,  CartesianGrid,  LabelList,} from "recharts"import { useChartReducedMotion } from "../shared/use-chart-reduced-motion"import { ChartContainer } from "../shared/chart-container"import {  ChartEmptyState,  ChartErrorState,} from "../shared/chart-state"import { cn } from "@/lib/utils"/* -------------------------------------------------------------------------- *//*  Type Definitions & Contracts                                              *//* -------------------------------------------------------------------------- */export type NumericKeyOf<TData> = [keyof TData] extends [never]  ? string  : {      [K in keyof TData]: TData[K] extends number | null | undefined ? K : never    }[keyof TData] extends never  ? string  : {      [K in keyof TData]: TData[K] extends number | null | undefined ? K : never    }[keyof TData] & stringexport type PercentStackLayout = "vertical" | "horizontal"export type PercentStackValueLabel = "none" | "auto"export type PercentStackMissingPolicy = "incomplete" | "zero"export type ObservationCompositionState = "valid" | "incomplete" | "zero-total"/** * Additive contributor series definition for PercentStackBars. * Represents a discrete additive component of the categorical whole. */export interface PercentStackBarSeries<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Property key on data records representing contributor's raw numeric magnitude */  key: NumericKeyOf<TData>  /** Human-readable display label for tooltips, legend, and screen readers */  label: string  /** Explicit per-series color override (defaults to theme tokens: var(--chart-1), var(--chart-2), etc.) */  color?: string  /** Optional custom numeric formatter for raw measurement in tooltips */  valueFormatter?: (value: number) => string}export interface ResolvedPercentStackSeries<TData extends Record<string, unknown> = Record<string, unknown>> {  key: NumericKeyOf<TData>  label: string  color: string  valueFormatter?: (value: number) => string  originalIndex: number}/** * Inspected categorical record containing resolved raw measurements, derived shares, and total state. */export interface ActivePercentStackDatum<TData extends Record<string, unknown> = Record<string, unknown>> {  /** 0-based index of the category within caller-preserved order */  index: number  /** Discrete category label or value */  category: string | number  /** Original raw datum provided by caller */  raw: TData  /** Per-contributor raw numeric measurements (null indicates unavailable/missing) */  rawValues: Record<string, number | null>  /** Derived normalized proportional shares (0–100%) for visible contributors */  shares: Record<string, number | null>  /** Visible category total sum across currently visible contributors (null if incomplete) */  visibleRawTotal: number | null  /** Composition validity state */  state: ObservationCompositionState}export interface NormalizedPercentStackRow<TData = Record<string, unknown>> {  __category: string | number  __index: number  __raw: TData  __rawValues: Record<string, number | null>  __shares: Record<string, number | null>  __visibleRawTotal: number | null  __state: ObservationCompositionState  [key: string]: unknown}export interface PercentStackBarsProps<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Readonly array of categorical records. Caller order is strictly preserved. */  data: readonly TData[]  /** Property key defining the discrete category domain */  categoryKey: keyof TData & string  /** Array of two or more additive contributor series composing the category whole */  series: readonly PercentStackBarSeries<TData>[]  /**   * Orientation layout:   * - "vertical": Categories on horizontal X-axis, bars grow vertically to 100% (default).   * - "horizontal": Categories on vertical Y-axis, bars grow horizontally to 100%.   */  layout?: PercentStackLayout  /** Chart container height in pixels or CSS dimension string. (default: 340) */  height?: number | string  /** Space between distinct category stacks along the categorical axis in pixels. (default: 20) */  groupGap?: number  /** Maximum width/thickness for individual stacked bars in pixels. (default: 48) */  maxBarSize?: number  /** Whether to render subtle reference grid lines at 0%, 25%, 50%, 75%, 100%. (default: true) */  showGrid?: boolean  /** Whether to render the categorical axis ticks and line. (default: true) */  showXAxis?: boolean  /** Whether to render the percentage axis ticks (0% - 100%) and line. (default: true) */  showYAxis?: boolean  /** Whether to render the series legend. (default: true) */  showLegend?: boolean  /** Whether the legend allows clicking contributors to toggle visibility. (default: true) */  interactiveLegend?: boolean  /**   * Value label rendering policy:   * - "none": No inline numeric labels (default).   * - "auto": Render percentage labels inside segments when space permits.   */  valueLabel?: PercentStackValueLabel  /**   * Handling of missing or non-finite contributor observations:   * - "incomplete": (default) If any visible contributor is missing or invalid, omit stack geometry to prevent visual deception and mark composition unavailable.   * - "zero": Explicitly treat missing values as zero contribution (0%).   */  missingValuePolicy?: PercentStackMissingPolicy  /** Motion animation toggle or configuration. Respects prefers-reduced-motion. */  motion?: boolean | { duration?: number }  /** Optional custom percentage formatter for tooltips and labels (default: `${val.toFixed(1)}%`) */  percentageFormatter?: (value: number) => string  /** Optional custom category label formatter for axes and tooltips */  categoryFormatter?: (category: string | number) => string  /** Optional global raw value formatter fallback */  valueFormatter?: (value: number) => string  /** Callback fired when the active inspected category changes */  onActiveChange?: (active: ActivePercentStackDatum<TData> | null) => void  /** Optional additional CSS class for root wrapper */  className?: string}/* -------------------------------------------------------------------------- *//*  Algorithmic Helpers: Pure & Deterministic                                 *//* -------------------------------------------------------------------------- */export function isFiniteNumber(val: unknown): val is number {  return typeof val === "number" && Number.isFinite(val)}/** * Resolves canonical series definitions to concrete color tokens. * Crucial contract: Series index in original configured array determines color token. * Hiding one series will never shift another series' assigned color token. */export function resolvePercentStackSeries<TData extends Record<string, unknown>>(  series: readonly PercentStackBarSeries<TData>[]): ResolvedPercentStackSeries<TData>[] {  return series.map((s, index) => {    const paletteIndex = (index % 8) + 1    const defaultColor = `var(--chart-${paletteIndex})`    return {      key: s.key,      label: s.label || String(s.key),      color: s.color && s.color.trim() !== "" ? s.color : defaultColor,      valueFormatter: s.valueFormatter,      originalIndex: index,    }  })}export interface PercentStackNormalizationResult<TData> {  rows: NormalizedPercentStackRow<TData>[]  hasNegativeValues: boolean  negativeErrorDetails?: string}/** * Pure normalization pipeline for 100% categorical stacked bars: * 1. Checks for negative values (V1 strictly rejects negatives; additive composition only). * 2. Missing values: under "incomplete", any missing contributor marks category unavailable. *    Under "zero", missing values are coerced to 0. * 3. Zero total (sum = 0): shares are null / 0, avoiding divide-by-zero or NaN. * 4. Normalizes visible series so they sum to exactly 100% of visible total. * 5. Input immutability strictly preserved (never mutates caller objects). */export function normalizePercentStackData<TData extends Record<string, unknown>>(  data: readonly TData[],  categoryKey: keyof TData & string,  resolvedSeries: readonly ResolvedPercentStackSeries<TData>[],  visibleKeys: ReadonlySet<string>,  missingValuePolicy: PercentStackMissingPolicy = "incomplete"): PercentStackNormalizationResult<TData> {  if (!Array.isArray(data) || data.length === 0) {    return { rows: [], hasNegativeValues: false }  }  const rows: NormalizedPercentStackRow<TData>[] = []  for (let i = 0; i < data.length; i++) {    const d = data[i]    if (!d || typeof d !== "object") {      rows.push({        __category: `Item ${i + 1}`,        __index: i,        __raw: d,        __rawValues: {},        __shares: {},        __visibleRawTotal: null,        __state: "incomplete",      })      continue    }    const rawCat = (d as Record<string, unknown>)[categoryKey]    const category = rawCat !== undefined && rawCat !== null ? String(rawCat) : `Item ${i + 1}`    const rawValues: Record<string, number | null> = {}    const shares: Record<string, number | null> = {}    let isComplete = true    let visibleRawTotal = 0    // First pass: extract and validate individual series measurements    for (const s of resolvedSeries) {      const rawVal = (d as Record<string, unknown>)[s.key as string]      if (typeof rawVal === "number") {        if (!Number.isFinite(rawVal)) {          rawValues[s.key] = null          if (visibleKeys.has(s.key)) isComplete = false        } else if (rawVal < 0) {          return {            rows: [],            hasNegativeValues: true,            negativeErrorDetails: `Percent Stack Bars requires non-negative raw contributions. Found negative value (${rawVal}) for series "${s.label}" at category "${category}".`,          }        } else {          rawValues[s.key] = rawVal          if (visibleKeys.has(s.key)) {            visibleRawTotal += rawVal          }        }      } else if (rawVal === null || rawVal === undefined) {        if (missingValuePolicy === "zero") {          rawValues[s.key] = 0        } else {          rawValues[s.key] = null          if (visibleKeys.has(s.key)) {            isComplete = false          }        }      } else {        // Unknown type or invalid string        rawValues[s.key] = null        if (visibleKeys.has(s.key)) isComplete = false      }    }    // Determine state    let state: ObservationCompositionState = "valid"    if (!isComplete) {      state = "incomplete"    } else if (visibleRawTotal === 0) {      state = "zero-total"    }    // Second pass: compute normalized 0–100% shares safely    for (const s of resolvedSeries) {      const val = rawValues[s.key]      if (!visibleKeys.has(s.key)) {        shares[s.key] = null      } else if (state === "valid" && visibleRawTotal > 0 && val !== null) {        // Proportional share in 0–100 range        shares[s.key] = (val / visibleRawTotal) * 100      } else if (state === "zero-total") {        shares[s.key] = 0      } else {        shares[s.key] = null      }    }    const rowObj: NormalizedPercentStackRow<TData> = {      __category: category,      __index: i,      __raw: d,      __rawValues: rawValues,      __shares: shares,      __visibleRawTotal: state === "incomplete" ? null : visibleRawTotal,      __state: state,    }    // Attach normalized keys for Recharts stacked Bar rendering    for (const s of resolvedSeries) {      if (visibleKeys.has(s.key) && state === "valid") {        rowObj[s.key] = shares[s.key] ?? 0      } else {        rowObj[s.key] = 0      }    }    rows.push(rowObj)  }  return { rows, hasNegativeValues: false }}/* -------------------------------------------------------------------------- *//*  Main Component: PercentStackBars                                          *//* -------------------------------------------------------------------------- */export function PercentStackBars<TData extends Record<string, unknown> = Record<string, unknown>>({  data,  categoryKey,  series,  layout = "vertical",  height = 340,  groupGap = 20,  maxBarSize = 48,  showGrid = true,  showXAxis = true,  showYAxis = true,  showLegend = true,  interactiveLegend = true,  valueLabel = "none",  missingValuePolicy = "incomplete",  motion = true,  percentageFormatter,  categoryFormatter,  valueFormatter: globalValueFormatter,  onActiveChange,  className,}: PercentStackBarsProps<TData>) {  /* ------------------------------------------------------------------------ */  /*  Hooks & State (strictly declared before early returns)                  */  /* ------------------------------------------------------------------------ */  const screenReaderId = React.useId()  const isReducedMotion = useChartReducedMotion()  const motionEnabled = !isReducedMotion && Boolean(motion)  const defaultShareFormatter = React.useCallback(    (val: number) => (percentageFormatter ? percentageFormatter(val) : `${val.toFixed(1)}%`),    [percentageFormatter]  )  const resolvedSeries = React.useMemo(() => {    return resolvePercentStackSeries(series || [])  }, [series])  // Interactive Legend: hidden series tracking (state only holds user-hidden keys)  const [hiddenKeys, setHiddenKeys] = React.useState<ReadonlySet<string>>(new Set())  // Derive visible series keys  const visibleKeys = React.useMemo(() => {    return new Set(      resolvedSeries        .filter((s) => !hiddenKeys.has(s.key))        .map((s) => s.key)    )  }, [resolvedSeries, hiddenKeys])  // Normalize data with pure deterministic helper  const normalizationResult = React.useMemo(() => {    return normalizePercentStackData(      data || [],      categoryKey,      resolvedSeries,      visibleKeys,      missingValuePolicy    )  }, [data, categoryKey, resolvedSeries, visibleKeys, missingValuePolicy])  const { rows, hasNegativeValues, negativeErrorDetails } = normalizationResult  // Active category inspection state  const [activeIndex, setActiveIndex] = React.useState<number | null>(null)  const [activeSeriesKey, setActiveSeriesKey] = React.useState<string | null>(null)  // Active datum resolution  const activeDatum = React.useMemo<ActivePercentStackDatum<TData> | null>(() => {    if (activeIndex === null || activeIndex < 0 || activeIndex >= rows.length) {      return null    }    const row = rows[activeIndex]    return {      index: row.__index,      category: row.__category,      raw: row.__raw,      rawValues: row.__rawValues,      shares: row.__shares,      visibleRawTotal: row.__visibleRawTotal,      state: row.__state,    }  }, [activeIndex, rows])  React.useEffect(() => {    onActiveChange?.(activeDatum)  }, [activeDatum, onActiveChange])  // Legend visibility toggle handler  const handleToggleSeries = React.useCallback(    (key: string) => {      if (!interactiveLegend) return      setHiddenKeys((prev) => {        const next = new Set(prev)        if (next.has(key)) {          next.delete(key)        } else {          next.add(key)        }        return next      })    },    [interactiveLegend]  )  const handleShowAll = React.useCallback(() => {    setHiddenKeys(new Set())  }, [])  // Keyboard navigation & layout orientation mapping  const isVertical = layout === "vertical"  const rechartsLayout = isVertical ? "horizontal" : "vertical"  const handleKeyDown = React.useCallback(    (e: React.KeyboardEvent<HTMLDivElement>) => {      if (rows.length === 0) return      const prevKey = isVertical ? "ArrowLeft" : "ArrowUp"      const nextKey = isVertical ? "ArrowRight" : "ArrowDown"      if (e.key === prevKey) {        e.preventDefault()        setActiveIndex((prev) => (prev === null || prev <= 0 ? rows.length - 1 : prev - 1))      } else if (e.key === nextKey) {        e.preventDefault()        setActiveIndex((prev) => (prev === null || prev >= rows.length - 1 ? 0 : prev + 1))      } else if (e.key === "Home") {        e.preventDefault()        setActiveIndex(0)      } else if (e.key === "End") {        e.preventDefault()        setActiveIndex(rows.length - 1)      } else if (e.key === "Escape") {        e.preventDefault()        setActiveIndex(null)        setActiveSeriesKey(null)      }    },    [rows.length, isVertical]  )  // Visible series in canonical order  const visibleSeriesList = React.useMemo(() => {    return resolvedSeries.filter((s) => visibleKeys.has(s.key))  }, [resolvedSeries, visibleKeys])  const outermostSeriesKey = React.useMemo(() => {    return visibleSeriesList.length > 0 ? visibleSeriesList[visibleSeriesList.length - 1].key : null  }, [visibleSeriesList])  /* ------------------------------------------------------------------------ */  /*  Early Return States (only after all hooks are declared)                */  /* ------------------------------------------------------------------------ */  if (hasNegativeValues) {    return (      <ChartErrorState        title="Negative Values Unsupported"        description={          negativeErrorDetails ||          "Percent Stack Bars requires non-negative additive values. Found negative numeric values in dataset."        }        className={className}      />    )  }  if (!data || data.length === 0 || !resolvedSeries || resolvedSeries.length === 0) {    return (      <ChartEmptyState        title="No Composition Data"        description="Provide categorical data and at least two additive series to render 100% stacked bars."        className={className}      />    )  }  // All contributors hidden by user  const allHidden = visibleSeriesList.length === 0  /* ------------------------------------------------------------------------ */  /*  Axes & Scales Setup                                                     */  /* ------------------------------------------------------------------------ */  const percentTicks = [0, 25, 50, 75, 100]  const formatCategory = (cat: string | number) => {    return categoryFormatter ? categoryFormatter(cat) : String(cat)  }  const formatPercentageAxis = (val: number) => `${val}%`  /* ------------------------------------------------------------------------ */  /*  Accessibility Narration & Table Data                                    */  /* ------------------------------------------------------------------------ */  const activeAnnouncement = activeDatum    ? activeDatum.state === "zero-total"      ? `${formatCategory(activeDatum.category)}. Raw total 0. Percentage composition unavailable because the visible total is zero.`      : activeDatum.state === "incomplete"      ? `${formatCategory(activeDatum.category)}. Percentage composition unavailable because the category is incomplete.`      : `${formatCategory(activeDatum.category)}. ${resolvedSeries          .filter((s) => visibleKeys.has(s.key))          .map((s) => {            const share = activeDatum.shares[s.key]            const rawVal = activeDatum.rawValues[s.key]            const shareStr = share !== null && share !== undefined ? defaultShareFormatter(share) : "unavailable"            const rawStr =              rawVal !== null && rawVal !== undefined                ? s.valueFormatter                  ? s.valueFormatter(rawVal)                  : globalValueFormatter                  ? globalValueFormatter(rawVal)                  : rawVal.toLocaleString()                : "unavailable"            return `${s.label} ${shareStr}, ${rawStr}`          })          .join(". ")}. Raw total, ${          activeDatum.visibleRawTotal !== null ? activeDatum.visibleRawTotal.toLocaleString() : "unavailable"        }.`    : ""  return (    <figure      role="region"      aria-label="100% Normalized Percent Stack Bars"      tabIndex={0}      onKeyDown={handleKeyDown}      onBlur={() => {        setActiveIndex(null)        setActiveSeriesKey(null)      }}      className={cn(        "group relative flex flex-col w-full focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 rounded-xl",        className      )}      style={{        height: typeof height === "number" ? `${height}px` : height,        minHeight: typeof height === "number" ? height : 340,        touchAction: "pan-y",      }}    >      {/* Live Region for Screen Readers */}      <div className="sr-only" aria-live="polite" aria-atomic="true">        {activeAnnouncement}      </div>      {/* Offscreen HTML Table Alternative */}      <div id={screenReaderId} className="sr-only">        <table>          <caption>100% Normalized Categorical Composition Data</caption>          <thead>            <tr>              <th scope="col">Category</th>              {resolvedSeries.map((s) => (                <React.Fragment key={s.key}>                  <th scope="col">{s.label} Raw</th>                  <th scope="col">{s.label} Share</th>                </React.Fragment>              ))}              <th scope="col">Visible Raw Total</th>              <th scope="col">Status</th>            </tr>          </thead>          <tbody>            {rows.map((r) => (              <tr key={r.__index}>                <th scope="row">{formatCategory(r.__category)}</th>                {resolvedSeries.map((s) => {                  const rawVal = r.__rawValues[s.key]                  const share = r.__shares[s.key]                  const rawStr =                    rawVal !== null && rawVal !== undefined                      ? s.valueFormatter                        ? s.valueFormatter(rawVal)                        : globalValueFormatter                        ? globalValueFormatter(rawVal)                        : rawVal.toLocaleString()                      : "Unavailable"                  const shareStr =                    share !== null && share !== undefined ? defaultShareFormatter(share) : "Unavailable"                  return (                    <React.Fragment key={s.key}>                      <td>{rawStr}</td>                      <td>{shareStr}</td>                    </React.Fragment>                  )                })}                <td>                  {r.__visibleRawTotal !== null ? r.__visibleRawTotal.toLocaleString() : "Unavailable"}                </td>                <td>                  {r.__state === "valid"                    ? "Complete"                    : r.__state === "zero-total"                    ? "Zero Total"                    : "Incomplete"}                </td>              </tr>            ))}          </tbody>        </table>      </div>      {/* Main Chart Container */}      <ChartContainer className="w-full flex-1 min-w-0 min-h-0 relative">        {allHidden ? (          <div className="flex flex-col items-center justify-center h-full w-full p-6 text-center space-y-3">            <span className="text-sm font-medium text-muted-foreground">              All series are currently hidden.            </span>            <button              type="button"              onClick={handleShowAll}              className="text-xs font-semibold px-3 py-1.5 rounded-lg border border-primary/30 text-primary hover:bg-primary/10 transition-colors"            >              Show all contributors            </button>          </div>        ) : (          <ResponsiveContainer            width="100%"            height="100%"            minWidth={0}            minHeight={0}            initialDimension={{ width: 320, height: typeof height === "number" ? height : 340 }}          >            <BarChart              data={rows}              layout={rechartsLayout}              barGap={0}              barCategoryGap={groupGap}              margin={                isVertical                  ? { top: 16, right: 16, left: 16, bottom: 8 }                  : { top: 16, right: 24, left: 16, bottom: 8 }              }              onMouseMove={(state) => {                if (state && state.activeTooltipIndex !== undefined) {                  const idx = Number(state.activeTooltipIndex)                  if (!Number.isNaN(idx) && idx >= 0 && idx < rows.length) {                    setActiveIndex(idx)                  }                }              }}              onMouseLeave={() => {                setActiveIndex(null)                setActiveSeriesKey(null)              }}            >              {showGrid && (                <CartesianGrid                  strokeDasharray="3 3"                  vertical={!isVertical}                  horizontal={isVertical}                  stroke="var(--border)"                  opacity={0.4}                />              )}              {isVertical ? (                <>                  <XAxis                    dataKey="__category"                    hide={!showXAxis}                    tickLine={false}                    axisLine={{ stroke: "var(--border)", opacity: 0.5 }}                    tick={{ fill: "var(--foreground)", fontSize: 11 }}                    tickFormatter={formatCategory}                  />                  <YAxis                    domain={[0, 100]}                    ticks={percentTicks}                    hide={!showYAxis}                    tickLine={false}                    axisLine={{ stroke: "var(--border)", opacity: 0.5 }}                    tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}                    tickFormatter={formatPercentageAxis}                    width={44}                  />                </>              ) : (                <>                  <XAxis                    type="number"                    domain={[0, 100]}                    ticks={percentTicks}                    hide={!showXAxis}                    tickLine={false}                    axisLine={{ stroke: "var(--border)", opacity: 0.5 }}                    tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}                    tickFormatter={formatPercentageAxis}                  />                  <YAxis                    dataKey="__category"                    type="category"                    hide={!showYAxis}                    tickLine={false}                    axisLine={{ stroke: "var(--border)", opacity: 0.5 }}                    tick={{ fill: "var(--foreground)", fontSize: 11 }}                    tickFormatter={formatCategory}                    width={80}                  />                </>              )}              {/* Stacked Bars in Canonical Order */}              {visibleSeriesList.map((s) => {                const isOutermost = s.key === outermostSeriesKey                const radius: [number, number, number, number] = isOutermost                  ? isVertical                    ? [4, 4, 0, 0]                    : [0, 4, 4, 0]                  : [0, 0, 0, 0]                return (                  <Bar                    key={s.key}                    dataKey={s.key as string}                    stackId="percent"                    name={s.label}                    fill={s.color}                    stroke="var(--background, #09090b)"                    strokeWidth={1}                    maxBarSize={maxBarSize}                    radius={radius}                    isAnimationActive={motionEnabled}                    animationDuration={motionEnabled ? 450 : 0}                    animationEasing="ease-out"                    onMouseEnter={() => setActiveSeriesKey(s.key)}                    onMouseLeave={() => setActiveSeriesKey(null)}                  >                    {valueLabel === "auto" && (                      <LabelList                        dataKey={s.key as string}                        position="center"                        fill="#ffffff"                        fontSize={9}                        formatter={(val: unknown) => {                          if (typeof val !== "number" || !Number.isFinite(val) || val < 8) return ""                          return `${Math.round(val)}%`                        }}                      />                    )}                  </Bar>                )              })}              {/* Canonical Synchronized Tooltip */}              <Tooltip                isAnimationActive={false}                allowEscapeViewBox={{ x: false, y: false }}                cursor={{                  fill: "var(--foreground)",                  opacity: 0.05,                }}                content={({ active, payload }) => {                  if (!active || !payload || payload.length === 0) return null                  const row = payload[0]?.payload as NormalizedPercentStackRow<TData> | undefined                  if (!row) return null                  const isZeroTotal = row.__state === "zero-total"                  const isIncomplete = row.__state === "incomplete"                  return (                    <div                      role="tooltip"                      className="plotcn-chart-tooltip rounded-xl border border-white/[0.12] bg-zinc-950/95 p-3.5 shadow-2xl backdrop-blur-md min-w-[min(200px,calc(100cqw-16px))] max-w-[min(300px,calc(100cqw-16px))] max-h-[calc(100cqh-16px)] overflow-y-auto text-xs font-sans not-prose space-y-2.5"                    >                      {/* Category Header */}                      <div className="flex items-center justify-between border-b border-white/[0.08] pb-2">                        <span className="font-semibold text-zinc-100 text-sm tracking-tight">                          {formatCategory(row.__category)}                        </span>                        <span                          className={cn(                            "px-1.5 py-0.5 rounded text-[10px] font-mono uppercase tracking-wider font-semibold",                            row.__state === "valid"                              ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"                              : row.__state === "zero-total"                              ? "bg-zinc-800/80 text-zinc-400 border border-zinc-700/50"                              : "bg-amber-500/10 text-amber-400 border border-amber-500/20"                          )}                        >                          {row.__state === "valid"                            ? "100% Normalized"                            : row.__state === "zero-total"                            ? "Zero Total"                            : "Incomplete"}                        </span>                      </div>                      {/* Composition Status Notice if not valid */}                      {isZeroTotal && (                        <p className="text-[11px] text-zinc-400 italic">                          Total measurement is zero. Proportional share is undefined.                        </p>                      )}                      {isIncomplete && (                        <p className="text-[11px] text-amber-400/90 italic">                          One or more visible series is unavailable. Normalized composition omitted.                        </p>                      )}                      {/* Series Rows in Canonical Order */}                      <div className="space-y-1.5">                        {resolvedSeries                          .filter((s) => visibleKeys.has(s.key))                          .map((s) => {                            const rawVal = row.__rawValues[s.key]                            const share = row.__shares[s.key]                            const isInspected = activeSeriesKey === s.key                            const shareDisplay =                              share !== null && share !== undefined                                ? defaultShareFormatter(share)                                : "—"                            const rawDisplay =                              rawVal !== null && rawVal !== undefined                                ? s.valueFormatter                                  ? s.valueFormatter(rawVal)                                  : globalValueFormatter                                  ? globalValueFormatter(rawVal)                                  : rawVal.toLocaleString()                                : "unavailable"                            return (                              <div                                key={s.key}                                className={cn(                                  "flex items-center justify-between gap-3 px-1.5 py-1 rounded transition-colors",                                  isInspected ? "bg-white/[0.08]" : "hover:bg-white/[0.04]"                                )}                              >                                <div className="flex items-center gap-2 min-w-0">                                  <span                                    className="size-2 rounded-full shrink-0"                                    style={{ backgroundColor: s.color }}                                    aria-hidden="true"                                  />                                  <span className="font-medium text-zinc-300 truncate">                                    {s.label}                                  </span>                                </div>                                <div className="flex items-center gap-2 text-right shrink-0">                                  <span className="font-semibold text-zinc-100 font-mono">                                    {shareDisplay}                                  </span>                                  <span className="text-[10px] text-zinc-500 font-mono">                                    ({rawDisplay})                                  </span>                                </div>                              </div>                            )                          })}                      </div>                      {/* Visible Raw Total Footer */}                      <div className="border-t border-white/[0.08] pt-2 flex items-center justify-between text-[11px]">                        <span className="text-zinc-400 font-medium">Visible Raw Total</span>                        <span className="font-mono font-semibold text-zinc-200">                          {row.__visibleRawTotal !== null                            ? globalValueFormatter                              ? globalValueFormatter(row.__visibleRawTotal)                              : row.__visibleRawTotal.toLocaleString()                            : "Unavailable"}                        </span>                      </div>                    </div>                  )                }}              />            </BarChart>          </ResponsiveContainer>        )}      </ChartContainer>      {/* Series Legend with Interactive Visibility Toggle */}      {showLegend && (        <div className="mt-3 shrink-0 flex flex-wrap items-center justify-center gap-3 text-xs">          {resolvedSeries.map((s) => {            const isVisible = visibleKeys.has(s.key)            const isInspected = activeSeriesKey === s.key            return (              <button                key={s.key}                type="button"                disabled={!interactiveLegend}                onClick={() => handleToggleSeries(s.key)}                onMouseEnter={() => setActiveSeriesKey(s.key)}                onMouseLeave={() => setActiveSeriesKey(null)}                className={cn(                  "inline-flex items-center gap-2 px-2.5 py-1 rounded-md border transition-all text-xs",                  interactiveLegend                    ? "cursor-pointer hover:bg-white/[0.06] active:scale-95"                    : "cursor-default",                  isVisible                    ? isInspected                      ? "border-primary/50 bg-primary/10 text-foreground font-medium"                      : "border-border/60 bg-background/50 text-foreground"                    : "border-border/20 bg-muted/20 text-muted-foreground/50 line-through opacity-60"                )}                aria-pressed={isVisible}                aria-label={`Toggle series ${s.label}`}              >                <span                  className={cn(                    "size-2.5 rounded-full shrink-0 transition-opacity",                    !isVisible && "opacity-30"                  )}                  style={{ backgroundColor: s.color }}                  aria-hidden="true"                />                <span className="truncate">{s.label}</span>              </button>            )          })}        </div>      )}    </figure>  )}