022 / RECHARTS / BAR & COLUMN
Percent Stack Bars
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
Checking public registry…
View local registry JSONCopied as source into your project (requires recharts).
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).
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 |
|---|---|---|---|
| Signal Bars (019) | bar-signal | "How do discrete categories compare on single or grouped measures?" | Grounded Zero () |
| Rank Bars (020) | bar-rank | "Which categories perform highest or lowest in top-N rank?" | Grounded Zero () |
| Group Compare (021) | bar-group-compare | "How do peer measures compare directly against each other?" | Shared Grounded Zero () |
| Stack Ledger (022) | bar-stack-ledger | "How do additive contributors compose the total magnitude?" | Additive Total () |
| Percent Stack (023) | bar-percent-stack | "How does the internal percentage composition differ across categories?" | Fixed Normalized () |
Absolute Stack vs. Percent Stack
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.
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:
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.
- Consumer Raw Data: Caller supplies raw additive non-negative numbers. Caller arrays remain strictly immutable.
- Contract & Color Resolution: Contributor colors map directly from configured series indices (
--chart-1,--chart-2, etc.) and remain fixed. - Non-Negative Validation: Any negative value () halts normalization and triggers a truthful error state.
- Visibility Filtering: Active legend toggles determine which contributors participate in the denominator.
- Denominator Calculation: Visible contributor raw values are summed to calculate the visible raw total.
- Proportional Allocation: For positive totals (), each contributor receives .
- Fixed 0–100 Stacking: Recharts renders normalized values on a fixed quantitative axis with ticks at
0%,25%,50%,75%, and100%.
Raw Values & Derived Shares
Percentages are always derived from canonical raw numbers, never ingested as source data:
Raw Measurement vs Derived Proportional Share
- 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 ()?
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 () 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
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 ()
- 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"): WhenmissingValuePolicy="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%:
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 .
- Series A becomes .
- Series B becomes .
- The visible stack continues to occupy exactly 100% of the bar extent.
- Series colors never shift. Series A remains
--chart-1and 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
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:
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
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:
- Broad Category Band: Hovering anywhere within the category column triggers the unified category tooltip, listing all contributors, shares, and totals.
- Individual Segment Hover: Moving directly over a specific segment highlights that series row within the tooltip and emphasizes the corresponding legend item.
- No Artificial Inflation: Plotcn never artificially inflates small segments to a minimum pixel height, preserving mathematical truthfulness.
Vertical vs. Horizontal Orientation
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:
<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)").
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.
<PercentStackBars
data={data}
categoryKey="segment"
series={[
{ key: "monthly", label: "Monthly" },
{ key: "annual", label: "Annual" },
{ key: "multiYear", label: "Multi-year" },
]}
/>Orientation layout: "vertical" (categories on X, stacks grow upward to 100%) or "horizontal" (categories on Y, stacks grow rightward).
layout="vertical"Default: "vertical"Space between adjacent category stacks along the categorical axis in pixels.
groupGap={20}Default: 20Maximum width/thickness for individual stacked bars in pixels.
maxBarSize={48}Default: 48Whether to render subtle reference grid lines at 0%, 25%, 50%, 75%, 100%.
showGrid={true}Default: trueWhether to render the series identity legend.
showLegend={true}Default: trueWhether clicking legend items toggles series visibility. Hiding contributors dynamically renormalizes remaining visible series to 100%.
interactiveLegend={true}Default: trueValue label rendering mode: "none" or "auto" (render percentage labels inside segments when space permits).
valueLabel="none"Default: "none"Handling of missing values: "incomplete" marks composition unavailable and suppresses stack geometry; "zero" coerces missing values to 0.
missingValuePolicy="incomplete"Default: "incomplete"| Property | Type | Default | Required | Description |
|---|---|---|---|---|
dataReq | readonly TData[] | [] | Yes | Readonly array of categorical records. Caller data is immutable and never modified. |
categoryKeyReq | keyof TData & string | — | Yes | Property key on data records representing the discrete category label. |
seriesReq | readonly PercentStackBarSeries<TData>[] | [] | Yes | Array of two or more additive series definitions composing the category whole. |
layoutOpt | "vertical" | "horizontal" | "vertical" | No | Orientation layout: "vertical" (categories on X, stacks grow upward to 100%) or "horizontal" (categories on Y, stacks grow rightward). |
heightOpt | number | string | 340 | No | Chart container height in pixels or CSS dimension string. |
groupGapOpt | number | 20 | No | Space between adjacent category stacks along the categorical axis in pixels. |
maxBarSizeOpt | number | 48 | No | Maximum width/thickness for individual stacked bars in pixels. |
showGridOpt | boolean | true | No | Whether to render subtle reference grid lines at 0%, 25%, 50%, 75%, 100%. |
showXAxisOpt | boolean | true | No | Whether to render the categorical axis (vertical) or percentage scale (horizontal). |
showYAxisOpt | boolean | true | No | Whether to render the percentage scale (vertical) or categorical axis (horizontal). |
showLegendOpt | boolean | true | No | Whether to render the series identity legend. |
boolean | true | No | Whether clicking legend items toggles series visibility. Hiding contributors dynamically renormalizes remaining visible series to 100%. | |
valueLabelOpt | "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. | |
motionOpt | boolean | { duration?: number } | true | No | Animation configuration. Automatically disabled under prefers-reduced-motion. |
(value: number) => string | val => `${val.toFixed(1)}%` | No | Custom formatter function for normalized share percentages. | |
(category: string | number) => string | String(category) | No | Custom formatter function for categorical axis labels. | |
(value: number) => string | value.toLocaleString() | No | Global fallback formatter function for raw numeric values in tooltips. | |
(active: ActivePercentStackDatum<TData> | null) => void | — | No | Callback fired when the active inspected category changes. |
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.
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.
Horizontal Enterprise Layout
Explicit horizontal orientation recommended for lengthy category labels, keeping text unclipped and readable.
Missing Contributor Incomplete State
Demonstrating how missing data in a category invalidates composition by default to prevent visual deception.
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.
Skeletons indicate runtime fetch or pending data queries.
Handles empty collections ([]) gracefully without crashing.
Graceful failure banner when data source or script fails.
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.
Full category tick density, inline percentage labels where configured, and comprehensive synchronized tooltips.
Adaptive category tick thinning, compact margins, and preserved 44px touch interaction targets.
Strict vertical page scrolling (pan-y), legend wrapped into compact rows, and automatic hiding of inline segment labels to prevent clutter.
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.
Container mounts as region with explicit assistive label.
Consistent canonical stack order, 1px structural segment boundaries, legend labels, and full off-screen HTML data table ensure complete non-color accessibility.
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.”
All initial entrance animations are bypassed immediately when prefers-reduced-motion is detected.
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Traverses categories in horizontal X order (vertical layout). Updates live region with category, raw measurements, and derived percentages. |
| ArrowUp / ArrowDown | Traverses categories in vertical Y order (horizontal layout). |
| Home / End | Jumps focus directly to the first or last categorical record. |
| Escape | Dismisses 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
seriesconfig order across all categories. - [x] Non-negative contract enforced; negative values halt rendering with truthful feedback.
- [x] Zero-total categories never invent equal shares ().
- [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
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:
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 TableProps Reference
| Property | Type | Default | Required | Description |
|---|---|---|---|---|
data | readonly TData[] | — | Required | Readonly array of categorical records. |
categoryKey | keyof TData & string | — | Required | Field name representing the categorical domain. |
series | readonly PercentStackBarSeries<TData>[] | — | Required | Additive contributor series definitions (key, label, optional color). |
layout | "vertical" | "horizontal" | "vertical" | Optional | Stacking orientation. |
height | number | string | 340 | Optional | Container height in pixels or CSS dimension string. |
groupGap | number | 20 | Optional | Spacing between category bars in pixels. |
maxBarSize | number | 48 | Optional | Maximum thickness of stacked bars in pixels. |
showGrid | boolean | true | Optional | Whether to display percentage grid reference lines. |
showLegend | boolean | true | Optional | Whether to render the series legend. |
interactiveLegend | boolean | true | Optional | Whether clicking legend items toggles visibility and renormalizes. |
valueLabel | "none" | "auto" | "none" | Optional | Whether to display inline percentage labels inside segments. |
missingValuePolicy | "incomplete" | "zero" | "incomplete" | Optional | Handling of missing or non-finite contributor values. |
percentageFormatter | (value: number) => string | — | Optional | Custom percentage formatter for tooltips and labels. |
categoryFormatter | (cat: string | number) => string | — | Optional | Custom formatter for category axis ticks and tooltips. |
motion | boolean | { duration?: number } | true | Optional | Animation toggle. Bypassed when prefers-reduced-motion is active. |
onActiveChange | (active: ActivePercentStackDatum | null) => void | — | Optional | Callback 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
Internal Architecture & File Dependencies
Source-first ownership model. Inspect the exact component call tree, dependencies, and full implementation below.
Handles container dimension measurement and SVG viewBox sizing
Coordinates scales, Cartesian grid, stacked Bar geometry with shared stackId='percent', and Tooltip