020 / RECHARTS / BAR & COLUMN
Group Compare Bars
Side-by-side peer measures within stable categorical groups, with shared quantitative scaling, deterministic series identity, and category-centric inspection.
- SPEC
- #020
- ENGINE
- Recharts
- FAMILY
- Bar & Column
- RENDERER
- svg
- STATUS
- preview
Installation
Checking public registry…
View local registry JSONCopied as source into your project (requires recharts).
Group Compare Bars compares peer quantitative measures side-by-side inside a shared categorical band. Category order and series identity remain stable, every visible series shares one quantitative scale, and missing values must never be disguised as zero-height bars.
021 / RECHARTS / BAR · SVG · GROUPED · MULTI-SERIES · RESPONSIVE · SOURCE-FIRSTQuick Facts
| Property | Value | Notes |
|---|---|---|
| Engine | Recharts | Cartesian coordinates via <BarChart>, <Bar>, <XAxis>, <YAxis> |
| Renderer | SVG | Crisp, scalable vector graphics across all DPRs |
| Composition | Grouped (Side-by-Side) | Discrete peer slots within each categorical band |
| Series Contract | 2+ peer measures | Ordered peer series sharing identical quantitative unit and scale |
| Category Order | Source order | Strictly preserved from caller data without auto-sorting |
| Series Order | Configuration order | Strictly preserved; never reorders by value magnitude |
| Quantitative Scale | Shared zero-anchored | Single Y-axis scale (or X-axis in horizontal); equal values yield equal lengths |
| Baseline | Zero (0) | Grounded reference line; positive bars extend up, negative extend down |
| Missing Policy | Slot reserved | Missing data renders no bar; slot remains reserved (no impersonation) |
| Zero Policy | Truthful observation | Value 0 renders 0-height bar; tooltip displays "0" |
| Legend | Visible by default | Canonical order; interactive visibility toggling supported |
| Inspection | Category-centric | Broad category band activation + precise bar rectangle focus |
| Accessibility | Dual-tier | 1 tab stop, ArrowLeft/Right (or Up/Down), live region, offscreen HTML <table> |
Installation
Install the component directly into your project using the shadcn CLI:
npx shadcn@latest add @plotcn/bar-group-compareDependencies
{ "dependencies": { "recharts": "^2.15.0" }, "registryDependencies": [ "@plotcn/chart-container", "@plotcn/chart-state", "@plotcn/chart-tooltip", "@plotcn/chart-motion" ]}Usage
import { GroupCompareBars } from "@/components/charts/recharts/bar-group-compare"const quarterlyRevenue = [ { quarter: "Q1", current: 184000, previous: 163000 }, { quarter: "Q2", current: 216000, previous: 191000 }, { quarter: "Q3", current: 228000, previous: 207000 }, { quarter: "Q4", current: 252000, previous: 236000 },]export function RevenueComparisonChart() { return ( <GroupCompareBars data={quarterlyRevenue} categoryKey="quarter" series={[ { key: "current", label: "Current Year", color: "var(--chart-1)" }, { key: "previous", label: "Previous Year", color: "var(--chart-2)" }, ]} valueFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} /> )}Comparison Model
In Group Compare Bars, each discrete category owns a shared comparison band. Inside that band, every configured peer series occupies a stable slot and extends independently from the common zero baseline.
Key Analytical Principles
- Peer Relationship: Series are treated as ordered peers rather than primary vs. reference or part-to-whole contributors.
- Independent Magnitudes: Each bar measures strictly from baseline zero. A grouped chart never sums values across series.
- Band Cohesion: Neighboring groups are separated by
groupGap, while peer bars inside one group are separated by a subtlebarGap.
Grouped vs. Stacked Semantics
The analytical difference between Group Compare Bars (021) and Stack Ledger Bars (022) is fundamental and must never be conflated behind a toggle:
- Grouped Bars (021) answer: "Within each category, how do multiple comparable measures differ from one another?" Bars sit side-by-side; each represents an independent quantity.
- Stacked Bars (022) answer: "What is the total magnitude for each category, and how do additive contributors compose that total?" Segments visually join into one cumulative whole.
Because GroupCompareBars has one dedicated analytical identity, generic stackMode props have been specialized away.
Stable Series Identity
A core invariant of Plotcn visualization is spatial predictability. Regardless of how values vary or cross between categories, series positions within the group never change.
Why Dynamic Sorting Is Forbidden
If Series A is higher than Series B in Q1, but Series B is higher than Series A in Q2, dynamically sorting bars inside each group would cause spatial positions to swap:
// FORBIDDEN: Unstable spatial swappingQ1: [ Slot 1: A (80) ] [ Slot 2: B (60) ]Q2: [ Slot 1: B (95) ] [ Slot 2: A (40) ] // Swapped! Cognitive dissonanceIn GroupCompareBars, Slot 1 is always Series A and Slot 2 is always Series B across all categories. The user's spatial memory is preserved.
Missing-Series Slot Reservation
Real-world datasets frequently contain unrecorded or missing data for one series in a given period.
The Slot Reservation Rule
When a series value is null, undefined, or non-finite:
- No Fake Bar: No rectangle of 0 height is fabricated.
- Slot Reserved: The spatial slot for that series remains reserved.
- No Impersonation: Neighboring surviving series do not shift left into the empty slot. Series C will never render in Slot B's position.
- Explicit Tooltip: The tooltip explicitly labels the metric as
"Unavailable"rather than hiding the row or falsely asserting zero.
Shared Quantitative Scale
Every configured peer series shares the exact same quantitative axis and scale.
Scale Integrity Guarantees
- Equal Lengths for Equal Values: A bar of value
100in Series A has the exact same quantitative length in pixels as a bar of value100in Series B. - No Dual Axes: Plotcn rejects dual-axis grouped bars because mismatched scales create optical illusions of parity between incompatible units (e.g. dollars vs. percentages).
- Mandatory Zero Anchor: Conventional bars encode magnitude via bar length. The quantitative domain automatically anchors at zero (
domainMin <= 0anddomainMax >= 0) with 8% headroom padding to prevent clipping.
Dual-Layer Hit Testing & Interaction
Grouped bars often contain narrow bars. Requiring a pixel-perfect hit on a thin rectangle to inspect a category frustrates pointer and touch exploration.
GroupCompareBars resolves this with a dual-layer hit architecture:
- Category Band Activation: Hovering anywhere within the broad category band highlights the group and presents the synchronized category tooltip.
- Precise Bar Rectangle Focus: Hovering directly over an individual bar highlights that specific series row in the tooltip and legend, maintaining category context without distorting bar geometry.
Responsive Layout Orientation
GroupCompareBars provides an intuitive layout prop with Plotcn semantics:
"vertical"(default): Categories along the X-axis; bars grow vertically."horizontal": Categories along the Y-axis; bars grow horizontally.
Guidelines for Orientation
- Use Vertical Layout for concise dates, quarters (Q1, Q2), months, or short codes.
- Use Horizontal Layout whenever category labels are lengthy (e.g. enterprise squads, department names, regional subsidiaries) to avoid unreadable slanted or clipped text.
Rendering Architecture
The data pipeline enforces validation, scale synchronization, and accessibility before rendering Recharts SVG primitives:
- Consumer Data Ingestion: Caller dataset and ordered
seriesarray ingested immutably. - Data Normalization & Validation: Non-finite values mapped to
null, caller category order preserved, duplicate category detection. - Canonical Color Resolution: Colors mapped deterministically from configured array index (
--chart-1,--chart-2, etc.). - Zero-Inclusive Shared Domain: Unified domain computed from min and max observed across all visible series.
- Recharts Grouped Cartesian Bars:
<BarChart>rendered withoutstackId, allocating side-by-side slots. - Dual-Tier Output: Synchronized category tooltip, interactive legend, keyboard focus, and screen reader HTML
<table>.
Component API & Props
Core Props
| Property | Type | Default | Description |
|---|---|---|---|
data * | readonly TData[] | — | Readonly array of categorical records. Caller data is never mutated. |
categoryKey * | keyof TData & string | — | Property key on records representing the discrete category domain. |
series * | readonly GroupCompareBarSeries<TData>[] | — | Ordered array of peer numeric series sharing unit and scale. |
layout | "vertical" | "horizontal" | "vertical" | Orientation. "vertical" = categories on X; "horizontal" = categories on Y. |
height | number | string | 340 | Container height in pixels or valid CSS string. |
domain | [number, number] | Auto [0, Max] | Quantitative scale override. Always anchored at zero by default. |
Appearance & Sizing
| Property | Type | Default | Description |
|---|---|---|---|
groupGap | number | 20 | Pixel gap between discrete category groups (barCategoryGap). |
barGap | number | 4 | Pixel gap between peer bars within one group (barGap). |
maxBarSize | number | 36 | Maximum thickness of individual bars in pixels. |
showGrid | boolean | true | Whether to display dashed reference grid lines. |
showXAxis | boolean | true | Whether to display the X-axis. |
showYAxis | boolean | true | Whether to display the Y-axis. |
showLegend | boolean | true | Whether to display the series legend. |
interactiveLegend | boolean | true | Whether clicking legend items toggles series visibility. |
valueLabel | "none" | "auto" | "always" | "none" | Value label display mode above bars. |
Formatting & Callbacks
| Property | Type | Default | Description |
|---|---|---|---|
motion | boolean | { duration?: number } | true | Entrance animation. Automatically disabled under reduced motion. |
categoryFormatter | (category: string | number) => string | String | Formatter function for categorical axis tick labels. |
valueFormatter | (value: number) => string | toLocaleString | Global formatter function for quantitative values. |
onActiveChange | (datum: ActiveGroupCompareDatum | null) => void | — | Callback fired when active category or series changes. |
Interactive Legend & Visibility
When interactiveLegend={true} is enabled:
- Clicking a series item toggles its visibility globally across all categories.
- Color Stability: Hiding Series 2 will never cause Series 3 to inherit Series 2's color. Colors remain bound to original configured series identity.
- All-Series-Hidden State: If all series are toggled off, a recoverable state displays: "All peer series are hidden" with a
"Show All Series"button.
Keyboard Navigation
GroupCompareBars implements accessible, single-tab-stop keyboard navigation:
- Focus: Press Tab to focus the chart region.
- Vertical Layout:
- →: Move to next category band.
- ←: Move to previous category band.
- Horizontal Layout:
- ↓: Move to next category band.
- ↑: Move to previous category band.
- Home / End: Jump directly to first or last category.
- Escape: Clear active category selection.
Live announcements narrate category name, current index, and all visible peer series values cleanly to screen readers.
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.
<GroupCompareBars
data={data}
categoryKey="quarter"
series={[
{ key: "current", label: "Current Year" },
{ key: "previous", label: "Previous Year" },
]}
/>Orientation of the chart. "vertical" places categories on X with vertical bars; "horizontal" places categories on Y with horizontal bars for long labels.
layout="vertical"Default: "vertical"Pixel gap between discrete category groups (barCategoryGap).
groupGap={20}Default: 20Pixel gap between peer bars within one categorical group.
barGap={4}Default: 4Maximum quantitative thickness of individual bars in pixels.
maxBarSize={36}Default: 36Whether to render subtle dashed reference grid lines.
showGrid={true}Default: trueWhether to display the interactive series legend.
showLegend={true}Default: trueWhether clicking legend items toggles series visibility.
interactiveLegend={true}Default: trueValue label rendering mode: "none", "auto" (fit-aware), or "always".
valueLabel="none"Default: "none"| Property | Type | Default | Required | Description |
|---|---|---|---|---|
dataReq | readonly TData[] | [] | Yes | Readonly array of categorical records. Caller data is never mutated or reordered. |
categoryKeyReq | keyof TData & string | — | Yes | Property name on data records representing the discrete category domain. |
seriesReq | readonly GroupCompareBarSeries<TData>[] | — | Yes | Array of peer numeric series sharing the same quantitative unit, domain, and scale. |
layoutOpt | "vertical" | "horizontal" | "vertical" | No | Orientation of the chart. "vertical" places categories on X with vertical bars; "horizontal" places categories on Y with horizontal bars for long labels. |
heightOpt | number | string | 340 | No | Height of the chart container in pixels or valid CSS string. |
domainOpt | [number, number] | Auto [min <= 0, max >= 0 + 8%] | No | Explicit quantitative domain override. Always anchored at zero by default. |
groupGapOpt | number | 20 | No | Pixel gap between discrete category groups (barCategoryGap). |
barGapOpt | number | 4 | No | Pixel gap between peer bars within one categorical group. |
maxBarSizeOpt | number | 36 | No | Maximum quantitative thickness of individual bars in pixels. |
showGridOpt | boolean | true | No | Whether to render subtle dashed reference grid lines. |
showXAxisOpt | boolean | true | No | Whether to display the categorical or numeric X-axis. |
showYAxisOpt | boolean | true | No | Whether to display the quantitative or categorical Y-axis. |
showLegendOpt | boolean | true | No | Whether to display the interactive series legend. |
boolean | true | No | Whether clicking legend items toggles series visibility. | |
valueLabelOpt | "none" | "auto" | "always" | "none" | No | Value label rendering mode: "none", "auto" (fit-aware), or "always". |
motionOpt | boolean | { duration?: number } | true | No | Animation configuration. Automatically disabled under prefers-reduced-motion. |
(category: string | number) => string | String(category) | No | Custom formatter function for categorical axis labels. | |
(value: number) => string | value.toLocaleString() | No | Global formatter function for numeric values. | |
(datum: ActiveGroupCompareDatum<TData> | null) => void | — | No | Callback fired when active category index or series key changes. |
Component Variants & Edge States
Production cookbooks showcasing configuration variants alongside verified handling of loading, empty data, and network error states.
Quarterly Revenue Comparison
Side-by-side comparison of Current Year vs. Previous Year quarterly performance.
Signups by Channel (3 Series)
Three-way comparison across Web, Mobile, and Partner channels.
Horizontal Enterprise Departments
Explicit horizontal orientation recommended for lengthy enterprise labels.
Missing Data Slot Reservation
Demonstrating how missing data reserves its slot without bar impersonation.
Mixed-Sign Value Comparison
Finite negative values extend downward from the shared zero baseline.
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
Group Compare Bars maintains full side-by-side peer comparison fidelity across all breakpoints down to 320px. Category ticks thin adaptively, while all configured peer series remain visible and truthful without silent removal.
Full category tick density, inline peer bars with full spacing, and comprehensive synchronized tooltips.
Adaptive category tick thinning, compact margins, and preserved 44px touch interaction targets.
Thinned category ticks, wrapped legend buttons, 44px touch targets, vertical page scroll preservation, and compact card tooltips.
Accessibility & Navigation Standards
Single tab stop on root figure with orientation-aware arrow navigation (Left/Right for vertical, Up/Down for horizontal), Home/End traversal, polite ARIA live announcements, and full off-screen structured HTML table for assistive devices.
Container mounts as region with explicit assistive label.
Deterministic horizontal/vertical series slot positions, explicit legend labels, category tooltips, and complete off-screen HTML table ensure complete non-color accessibility even when series share identical hues.
Embeds visually hidden summary (.sr-only) declaring: “Announces category name, group index, total categories, and all peer series values factually without subjective leader/winner assumptions.”
All initial entrance animations are bypassed immediately when prefers-reduced-motion is detected.
| Key | Action |
|---|---|
| ArrowRight / ArrowDown | Inspect next categorical group across the discrete domain. |
| ArrowLeft / ArrowUp | Inspect previous categorical group across the discrete domain. |
| Home | Jump inspection directly to the first category. |
| End | Jump inspection directly to the last category. |
| Escape | Clear active group inspection. |
Data Safety Checklist
- [x] Preserved Category Order: Source array order is strictly respected; no automatic sorting.
- [x] Preserved Series Order: Configured series order is invariant across all categories.
- [x] Shared Quantitative Scale: All peer series share one Y-axis scale; no dual axes.
- [x] Zero Baseline: Quantitative domain always includes zero; no truncated floating bars.
- [x] Zero Is Valid: Observations of
0render a zero-height bar, not an error or fake rectangle. - [x] Negative Support: Negative values extend downward from zero baseline truthfully.
- [x] Missing Slot Reservation: Missing values render no bar but preserve slot spacing.
- [x] No Bar Impersonation: Surviving bars never shift into an absent peer's position.
- [x] Stable Colors: Series colors derive from canonical configuration order, not visible index.
- [x] Recoverable Hidden State: Hiding all series displays an actionable recovery UI.
- [x] Responsive Integrity: Responsive adaptation reduces ticks and labels before content; never silently drops a series.
- [x] Immutable Data: Caller datasets and series arrays are never mutated.
Related Components
- 019 / Signal Bars — Canonical discrete categorical comparison with single or grouped series.
- 022 / Stack Ledger Bars — Additive stacked bars for cumulative composition and truthful totals.
Internal Architecture & File Dependencies
Source-first ownership model. Inspect the exact component call tree, dependencies, and full implementation below.
Manages container aspect ratio, dimensions, and fluid SVG scaling
Accessible toolbar with series color swatches and interactive visibility toggling
Offscreen HTML table providing complete data access for assistive technologies