027 / RECHARTS / BAR & COLUMN

Interactive Bars

Recharts

Category-first bar inspection with a precise band cursor, persistent touch locking, and keyboard traversal across single or grouped categorical series.

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

Installation

PLOTCN/REGISTRY/BAR-INTERACTIVE/SOURCE
pnpm dlx shadcn@latest add @plotcn/bar-interactive

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
Interactive Preview
Isolated Preview BoundaryViewport: DESKTOP

Overview

Interactive Bars is Plotcn's specialized categorical Bar chart engineered specifically for reliable data inspection across pointer, touch, and keyboard modalities.

Rather than treating interaction as an afterthought or ephemeral hover decoration, Interactive Bars makes the category band the primary interaction target:

Governing Principle: Interactive Bars is designed for reliable inspection, not merely hover decoration. The active category must remain understandable and controllable across pointer, touch, and keyboard input without requiring pixel-perfect bar hits, blocking page scroll, fabricating values, or tying interaction state to ephemeral Recharts tooltip internals.

Interaction Hierarchy

Animated Interaction Hierarchy: Category-First ArchitectureInteractive hierarchy illustrating how Category is the primary stable inspection unit, branching into Category Band (pointer, keyboard, touch lock) and subordinate Exact Series Identity.ROOT INTERACTION UNITCategoryPRIMARY TARGETcategory bandpointer inspectiontransientHover enters category band · Tooltip opens · Clears immediately on leavekeyboard traversalsingle tab stopArrow keys move focus · Home/End jump · Clamps at boundariestouch activation / lockpersistentSingle tap establishes locked state · Page scrolling remains fluid & nativeOPTIONAL REFINEMENTexact series identitySubordinate to category contexttooltip emphasis + active stroke• Emphasizes hovered bar rectangle• Never hides visible peer series• Preserves configured series order• Tooltip remains category-first
Figure: Interactive hierarchy showing Category as the primary stable unit, governing the category band and subordinate series mark emphasis.

Installation

PLOTCN/REGISTRY/BAR-INTERACTIVE/SOURCE
pnpm dlx shadcn@latest add @plotcn/bar-interactive

Checking public registry…

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

Copied as source into your project (requires recharts).

Usage

TSX
import { InteractiveBars } from "@/components/charts/recharts/bar-interactive"const data = [  { quarter: "Q1", product: 82, services: 54, enterprise: 38 },  { quarter: "Q2", product: 96, services: 61, enterprise: 44 },  { quarter: "Q3", product: 88, services: 67, enterprise: 52 },  { quarter: "Q4", product: 104, services: 72, enterprise: 58 },]export function RevenueMixExample() {  return (    <InteractiveBars      data={data}      categoryKey="quarter"      series={[        { key: "product", label: "Product Revenue", valueFormatter: (v) => `$${v}M` },        { key: "services", label: "Services Revenue", valueFormatter: (v) => `$${v}M` },        { key: "enterprise", label: "Enterprise Sales", valueFormatter: (v) => `$${v}M` },      ]}      showLegend      lockOnTouch      lockOnClick    />  )}

Interaction State Machine

The chart manages three clean states: IDLE, ACTIVE, and LOCKED.

Interactive Bars State MachineDiagram illustrating state transitions between IDLE, ACTIVE, and LOCKED states driven by pointer, touch, and keyboard input.01IDLENo active category02ACTIVETransient HoverClears on pointer leave03LOCKEDPersistent SelectionSurvives scroll & leavepointer enterpointer leaveclick / tap / SpaceTouch tap / Keyboard directEscape / tap again / dismiss
Figure 1: Complete three-state lifecycle distinguishing transient hover from persistent inspection.
  1. IDLE: The chart is resting with no active category.
  2. ACTIVE (Transient): A pointer enters a category band. The band lights up subtly and the tooltip opens. As soon as the pointer leaves the chart, the state resets to IDLE.
  3. LOCKED (Persistent): A user taps a category on touch, clicks with a mouse, or presses Enter/Space while traversing with keyboard. The category and tooltip remain pinned even after the cursor leaves or the page is scrolled.

Active vs Locked

Active vs Locked ModalitySide-by-side comparison showing transient active state on the left and persistent locked state on the right.ACTIVE STATETransient• Driven by pointer hover• Tooltip disappears immediately on pointer leave• Requires stationary cursor• Unsuitable for touch screen interaction• Subordinate to locked selectionLOCKED STATEPersistent• Established by tap, click, or Space/Enter• Tooltip remains pinned when finger/cursor leaves• Survives vertical page scrolling• Updates dynamically if underlying data changes• Dismissed cleanly via Escape or 2nd tap
Figure 2: Transient hover provides fluid desktop scanning, while persistent locking guarantees touch inspection.
  • Active: Designed for fluid desktop exploration. Moving between bars updates the active readout immediately without cluttering the screen when moving away.
  • Locked: Solves the fundamental flaw of touch interfaces where hovering does not exist. The user taps to lock, reads the values, scrolls the page to review text, and dismisses when ready.

Category Band vs Rectangle

Conventional charts require users to position their cursor directly over thin rectangular bars to trigger a tooltip. In grouped charts or dense data, this causes frustrating missed hits.

Category Band vs Rectangle Hit TargetIllustration of a wide category band hit area enclosing peer series bars versus narrow individual rectangles.Q1FULL CATEGORY BANDQ2 (Active)Exact Series HighlightSubordinate to category bandQ3
Figure 3: The category band is the primary interaction unit. Touching anywhere in the band activates the full category.

In Plotcn's Interactive Bars:

  • The entire category band acts as the primary hit area.
  • Hovering or tapping anywhere within the band activates the full category group.
  • The tooltip reports all visible peer series simultaneously.
  • Direct contact with a specific rectangle refines activeSeriesKey to emphasize that series without hiding peer comparisons.

Zero & Tiny Bars

Zero and Tiny Bar InspectabilityShows that zero and tiny bars remain fully interactive through category bands without distorting numeric geometry.0Normal ($100)Easy targetTiny ($0.01)Band hit area = 150pxNo fake bar inflationZero ($0.00)Fully inspectableTooltip reports truthful 0
Figure 4: Quantitative bar marks remain mathematically truthful without artificial minimum height or fake inflation.

In conventional bar charts, zero-valued bars have zero height and are impossible to hit. Designers often introduce dangerous "fake minimum bar heights" that visually falsify the data.

Plotcn strictly guarantees mathematical truthfulness:

  • Zero-valued bars are drawn with 0 height.
  • Tiny bars (e.g. 0.001) remain microscopically small.
  • Both remain 100% inspectable because the broad category band captures the interaction, and the tooltip truthfully reports $0.00 or $0.01.

Touch Lock Lifecycle

Touch Lock LifecycleFive-stage lifecycle showing tap activation, lock persistence, scroll safety, and clean dismissal.1Tap BarFinger touchescategory band2Lock SetTooltip pinnedBand highlighted3Finger LiftsLock preservedNo hover loss4Page ScrollScroll safeNo touch lockup5DismissTap again /tap other
Figure 5: Touch locking keeps inspection actionable on mobile without trapping page gestures.
  1. Tap Bar: Finger taps anywhere in the category band.
  2. Lock Established: The category band is outlined, and the tooltip is locked into place.
  3. Finger Lifts: The user removes their finger. The inspection state persists.
  4. Scroll Page: The user scrolls down to read accompanying text. Native scrolling is completely unimpeded (touch-action is never globally blocked).
  5. Dismiss / Transfer: Tapping another category transfers the lock; pressing Escape or tapping the locked category again dismisses the inspection.

Single Tab-Stop Keyboard Traversal

Plotcn avoids keyboard "tab explosions" where each bar creates an independent tab stop.

Single Tab-Stop Keyboard TraversalKeyboard navigation model using Left and Right arrows, Home, End, Enter, Space, and Escape.← →Next / PrevCategory traversalHome / EndExtremesFirst or last categoryEnter / SpaceToggle LockPin/unpin tooltipEscapeUnlockDismiss inspection!One Tab Stop ArchitectureFocus belongs to the figure container; bars do not generate individual tab stops.
Figure 6: One-tab-stop keyboard interaction prevents tab explosions while allowing exhaustive category traversal.
  • 1 Tab Stop: Pressing Tab focuses the entire chart container.
  • Arrow Keys: ArrowLeft / ArrowRight (vertical layout) or ArrowUp / ArrowDown (horizontal layout) step sequentially across categories.
  • Extremes: Home jumps to the first category, End jumps to the last.
  • Locking: Enter or Space locks the currently focused category; Escape unlocks it without losing focus.

Grouped-Series Inspection

Grouped Multi-Series ReadoutDemonstrates how the category tooltip reports all peer series values simultaneously when hovering a category.Q2 2026Q2 2026LockedProduct Revenue$96MServices Revenue ★$61MEnterprise$44MAll visible peer series disclosed in canonical order.
Figure 7: Inspecting an individual bar highlights that series without suppressing its peer comparisons.

When multiple peer series share a category:

  • The category band envelops the entire group.
  • The tooltip presents all visible series in their canonical configured order.
  • Exact bar contact highlights that individual series row with a star and bold readout while keeping the surrounding comparative numbers in view.

Input Modality Precedence

Input Modality Precedence HierarchyDeterministic resolution flow between touch, keyboard, and pointer inputs without race conditions.PRIORITY 1Locked StateSurvives all pointer motionUnlocked only by explicit actionPRIORITY 2Keyboard TraversalIntentional Arrow/Home/EndOverrides stale hover positionPRIORITY 3Pointer HoverTransient inspectionYields to lock & keys
Figure 8: Deterministic modality hierarchy prevents race conditions between mouse, touch, and keyboard.

When transitioning between modalities (e.g., hovering with a mouse, then using arrow keys, or tapping on a touchscreen):

  1. Locked State takes top priority: transient mouse movement outside will not dismiss a deliberately locked card.
  2. Keyboard Traversal takes second priority: moving an arrow key intentionally moves the active inspection.
  3. Pointer Hover is transient: it updates only when no category is actively locked.

Focus vs Active vs Locked

Visual Distinction between Focus, Active, and LockedThree distinct visual states: focus ring on chart shell, active band fill, and locked dashed structural border.FOCUSOuter Shell RingApplies to <figure>Keyboard tab targetACTIVESubtle Band FillUnderlying category bandHover cursor opacity 0.09LOCKEDDashed Border + TagPersistent selectionLock badge in tooltip
Figure 9: Never conflate shell focus, transient hover, and persistent lock into one generic outline.

To prevent user confusion, Plotcn establishes three visually distinct treatments:

  • FOCUS: Structural blue focus ring around the <figure> container for keyboard awareness.
  • ACTIVE: Subtle, low-opacity band fill (rgba(59, 130, 246, 0.09)) indicating transient pointer hover.
  • LOCKED: High-contrast dashed structural border (var(--chart-selection)), persistent background tint, and an explicit Locked badge in the tooltip header.

Interactive Legend

Interactive Legend Series TogglingDemonstrates how toggling series in the legend preserves category lock and stable color identity.Product (Visible)Services (Hidden)Enterprise (Visible)Stability Invariants Guaranteed by Plotcn:1. Hiding a series never recolors peers. 2. Locked category remains active. 3. Tooltip preserves canonical series order.
Figure 10: Toggling series visibility filters data rows without resetting interactive locks or reassigning palette slots.

When interactiveLegend={true} is enabled, clicking legend items toggles series visibility while preserving key invariants:

  • Hiding a series never shifts the palette colors of remaining series.
  • Active and locked categories remain intact.
  • The tooltip dynamically omits hidden series while truthfully reporting missing observations as "Unavailable".

Layout & Orientation

Vertical vs Horizontal Interaction AxesComparison of horizontal X category traversal vs vertical Y category traversal.Vertical Layout (Default)X-Axis Bands • ArrowLeft / ArrowRightHorizontal LayoutY-Axis Rows • ArrowUp / ArrowDown
Figure 11: Category bands and keyboard navigation adapt automatically to vertical or horizontal orientation.

Interactive Bars supports both vertical and horizontal layouts:

  • Vertical (layout="vertical"): Ideal for standard categorical timelines (quarters, months, categories) with left-to-right arrow navigation.
  • Horizontal (layout="horizontal"): Ideal for long category labels and mobile scorecards with top-to-bottom arrow navigation.

Rendering Architecture

Rendering and Interaction ArchitectureArchitecture pipeline from raw data validation through Recharts grouped bars, interaction overlay, state reducer, and accessibility tree.Consumer DataCategoryKey + Series[]Data NormalizerPreserve order • Filter NaNsRecharts CoreBarChart • Axes • GridINTERACTION REDUCERActive & Locked State MachinePointer • Touch tap • Keyboard arrowsDiscrete category snappingBand Cursor & TooltipControlled positionPersistent pinAccessibility Shellaria-live politeStructured HTML table
Figure 12: Separation of concerns: Recharts handles mark geometry while Plotcn's reducer controls inspection state and a11y.

The component cleanly decouples visual mark rendering from interaction state management:

  1. Recharts Core handles the SVG Cartesian axes, gridlines, and grouped <Bar> shapes.
  2. Interaction Reducer manages discrete category band coordinates, active series keys, and persistent lock state.
  3. Controlled Tooltip renders outside the Recharts event loop, ensuring tooltip stability across resize, theme change, and touch scrolling.
  4. Accessibility Shell synchronizes live polite announcements and structured HTML tables.
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):
<InteractiveBars
  data={data}
  categoryKey="quarter"
  series={[
    { key: "product", label: "Product" },
    { key: "services", label: "Services" },
    { key: "enterprise", label: "Enterprise" },
  ]}
/>
Interactive Prop Preview Lab
layout"vertical" | "horizontal"

Layout orientation: vertical (default) puts categories on X, horizontal puts categories on Y.

Select value to preview live:
Active: layout="vertical"Default: "vertical"
cursorMode"band-and-bar" | "band"

Cursor highlighting policy: full category band only or band plus active bar emphasis.

Select value to preview live:
Active: cursorMode="band-and-bar"Default: "band-and-bar"
showGridboolean

Whether to render subtle background Cartesian gridlines.

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

Whether to display the structural series legend.

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

Whether clicking legend items toggles series visibility.

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

Display mode for permanent numeric value labels.

Select value to preview live:
Active: valueLabel="none"Default: "none"
lockOnClickboolean

Whether mouse clicks establish a persistent inspection lock.

Select value to preview live:
Active: lockOnClick={true}Default: true
lockOnTouchboolean

Whether touch taps establish a persistent inspection lock.

Select value to preview live:
Active: lockOnTouch={true}Default: true
All Properties (14)
Component properties
PropertyTypeDefaultRequiredDescription
dataReq
readonly TData[][]Yes

Array of categorical data records. Order is strictly preserved.

keyof TData & stringYes

Key on data records representing the discrete category label.

readonly InteractiveBarSeries<TData>[]Yes

One or more peer numeric series definitions specifying key, label, and formatters.

"vertical" | "horizontal""vertical"No

Layout orientation: vertical (default) puts categories on X, horizontal puts categories on Y.

number | string340No

Container height in pixels or CSS height string.

[number, number] | "auto""auto"No

Quantitative domain policy. Defaults to zero-anchored extent covering all series values.

"band-and-bar" | "band""band-and-bar"No

Cursor highlighting policy: full category band only or band plus active bar emphasis.

booleantrueNo

Whether to render subtle background Cartesian gridlines.

booleanfalseNo

Whether to display the structural series legend.

booleanfalseNo

Whether clicking legend items toggles series visibility.

"none" | "auto""none"No

Display mode for permanent numeric value labels.

booleantrueNo

Whether mouse clicks establish a persistent inspection lock.

booleantrueNo

Whether touch taps establish a persistent inspection lock.

boolean | { duration?: number }trueNo

Animation toggle honoring prefers-reduced-motion.

04 / Cookbook & States

Component Variants & Edge States

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

Quarterly Revenue Mix

Three peer revenue streams compared across four quarters with persistent touch lock.

<InteractiveBars
  data={[
    { quarter: "Q1", product: 82, services: 54, enterprise: 38 },
    { quarter: "Q2", product: 96, services: 61, enterprise: 44 },
    { quarter: "Q3", product: 88, services: 67, enterprise: 52 },
    { quarter: "Q4", product: 104, services: 72, enterprise: 58 },
  ]}
  categoryKey="quarter"
  series={[
    { key: "product", label: "Product", valueFormatter: (v) => `$${v}M` },
    { key: "services", label: "Services", valueFormatter: (v) => `$${v}M` },
    { key: "enterprise", label: "Enterprise", valueFormatter: (v) => `$${v}M` },
  ]}
  showLegend
/>

Zero and Micro Value Inspection

Proves that zero and tiny bars remain 100% inspectable without artificial geometry distortion.

<InteractiveBars
  data={[
    { category: "Item A", val1: 0, val2: 45 },
    { category: "Item B", val1: 0.001, val2: 80 },
    { category: "Item C", val1: 65, val2: 0 },
    { category: "Item D", val1: 95, val2: 110 },
  ]}
  categoryKey="category"
  series={[
    { key: "val1", label: "Primary", valueFormatter: (v) => `$${v}` },
    { key: "val2", label: "Secondary", valueFormatter: (v) => `$${v}` },
  ]}
  showLegend
/>

Missing Observations

Demonstrates that missing records are omitted without fake zero-substitution and reported truthfully.

<InteractiveBars
  data={[
    { quarter: "Q1", web: 120, mobile: 80 },
    { quarter: "Q2", web: 140, mobile: null },
    { quarter: "Q3", web: 165, mobile: 110 },
    { quarter: "Q4", web: null, mobile: 130 },
  ]}
  categoryKey="quarter"
  series={[
    { key: "web", label: "Web Requests" },
    { key: "mobile", label: "Mobile Requests" },
  ]}
  showLegend
/>

Horizontal Team Benchmarks

Horizontal orientation for long category labels and compact mobile screens.

<InteractiveBars
  data={[
    { team: "Core Infrastructure", p95: 42, p99: 88 },
    { team: "Payments & Invoicing", p95: 65, p99: 124 },
    { team: "Identity & Security", p95: 28, p99: 55 },
    { team: "Search & Retrieval", p95: 52, p99: 94 },
  ]}
  categoryKey="team"
  series={[
    { key: "p95", label: "p95 Latency", valueFormatter: (v) => `${v}ms` },
    { key: "p99", label: "p99 Latency", valueFormatter: (v) => `${v}ms` },
  ]}
  layout="horizontal"
  showLegend
/>
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

Container-driven responsiveness. Preserves category hit targets, full category bands, persistent inspection lock, and exact tooltips down to 320px mobile viewports without blocking native vertical scrolling.

Mobile Compact
< 440px

Category bands maintain generous touch targets; quantitative tick count is thinned accessibly; legend wraps cleanly.

Tablet / Medium
440px – 768px

Full category band hit regions allow comfortable touch activation and tap-to-lock inspection of all peer series.

Desktop Expanded
> 768px

Full layout displaying subtle Cartesian gridlines, hover cursor bands, exact series rectangle emphasis, and multi-series tooltip cards.

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

Accessibility & Navigation Standards

Accessible categorical bar chart with single tab stop, orientation-specific arrow-key traversal, Home/End navigation, Enter/Space persistent lock, Escape unlock, polite live region announcements, and offscreen structured HTML table.

Semantic Role & Landmark

Container mounts as region with explicit assistive label.

Color-Independent Legibility

Series identity is reinforced through grouped bar positions, tooltip labels, legend marks, and structured table data, remaining fully functional in monochrome.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “Announces category name, category index of total, and exact values for all peer series factually, including explicit 'Unavailable' announcements for missing records.

Reduced Motion Support

All entrance transitions and cursor animations are suppressed when prefers-reduced-motion is enabled.

Keyboard Interaction Model
Keyboard interaction model
KeyAction
ArrowRight / ArrowLeftTraverse categories in vertical layout
ArrowDown / ArrowUpTraverse categories in horizontal layout
HomeJump focus to the first category
EndJump focus to the last category
Enter / SpaceToggle persistent inspection lock on active category
EscapeUnlock inspection and dismiss tooltip

Data Safety Guarantees

Plotcn enforces strict operational safety rules:

  • Preserved Order: Category order matches caller order without automatic ranking or sorting.
  • Zero vs Missing: Zero is a valid number (0); missing (null or undefined) is explicitly rendered as "Unavailable".
  • No Geometry Inflation: Quantitative marks are never stretched to create larger hit targets; category bands provide the hit area.
  • Data Updates While Locked: If the underlying dataset updates while a category is locked, the tooltip immediately reflects the fresh values rather than showing a stale snapshot.
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
InteractiveBars(Semantic figure and interaction coordinator)
└──ChartContainer[Container query wrapper]

Provides responsive sizing and theme token scope

Involved Source Files & Registry Assets
registry/recharts/bar-interactive.tsx
Complete InteractiveBars component with state reducer, category-band cursor, touch lock, single-tab keyboard traversal, and accessible table.
registry/recharts/bar-interactive.tsx
"use client"import * as React from "react"import {  ResponsiveContainer,  BarChart,  Bar,  XAxis,  YAxis,  CartesianGrid,  Tooltip,  LabelList,  Customized,} from "recharts"import { cn } from "@/lib/utils"import { ChartContainer } from "@/registry/shared/chart-container"import { ChartEmptyState, ChartLoadingState, ChartUnavailableState } from "@/registry/shared/chart-state"import { HugeiconsIcon } from "@hugeicons/react"import { LockIcon, LockOpenIcon } from "@hugeicons/core-free-icons"/* -------------------------------------------------------------------------- *//*  Types & 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 interface InteractiveBarSeries<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Property on data record representing the numeric measure */  readonly key: NumericKeyOf<TData>  /** Human-readable label for the series */  readonly label: string  /** Explicit series color override (defaults to --chart-N) */  readonly color?: string  /** Value formatter for tooltip and labels */  readonly valueFormatter?: (value: number) => string}export type InteractiveBarLayout = "vertical" | "horizontal"export type InteractiveBarValueLabel = "none" | "auto"export type InteractiveBarCursorMode = "band" | "band-and-bar"export type InteractiveInputMode = "pointer" | "touch" | "keyboard" | nullexport interface PreparedInteractiveDatum<TData> {  __source: TData  __index: number  __category: string | number  [key: string]: unknown}export interface InteractiveBarsProps<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Array of categorical records. Caller order is strictly preserved. */  readonly data: readonly TData[]  /** Key on data records representing the discrete category label */  readonly categoryKey: keyof TData & string  /** One or more peer numeric series sharing the same category and scale */  readonly series: readonly InteractiveBarSeries<TData>[]  /** Layout orientation: "vertical" (categories on X) or "horizontal" (categories on Y). Default: "vertical" */  readonly layout?: InteractiveBarLayout  /** Container height in pixels or CSS dimension string (default: 340) */  readonly height?: number | string  /** Quantitative scale domain policy or explicit [min, max] override */  readonly domain?: [number, number] | "auto"  /** Whether to render subtle background Cartesian gridlines (default: true) */  readonly showGrid?: boolean  /** Whether to display the series legend (default: false) */  readonly showLegend?: boolean  /** Whether legend series items can be toggled to show/hide (default: false) */  readonly interactiveLegend?: boolean  /** Display mode for permanent value labels (default: "none") */  readonly valueLabel?: InteractiveBarValueLabel  /** Cursor interaction mode: category band only or band + active bar emphasis (default: "band-and-bar") */  readonly cursorMode?: InteractiveBarCursorMode  /** Whether touch taps establish a persistent inspection lock (default: true) */  readonly lockOnTouch?: boolean  /** Whether mouse clicks establish a persistent inspection lock (default: true) */  readonly lockOnClick?: boolean  /** Motion configuration (honors prefers-reduced-motion) */  readonly motion?: boolean | { duration?: number }  /** Additional CSS class names */  readonly className?: string  /** Semantic chart title for accessibility */  readonly title?: string  /** Analytical description for screen readers */  readonly description?: string  /** Whether the chart is currently loading data */  readonly loading?: boolean}/* -------------------------------------------------------------------------- *//*  Mathematical & Domain Helpers                                             *//* -------------------------------------------------------------------------- */export function isFiniteNumber(val: unknown): val is number {  return typeof val === "number" && Number.isFinite(val) && !Number.isNaN(val)}/** * Derives the quantitative domain covering all visible series values. * Truthfully includes zero for magnitude bar charts. */export function resolveInteractiveBarDomain<TData extends Record<string, unknown>>(  data: readonly TData[],  seriesKeys: readonly string[],  explicitDomain?: [number, number] | "auto"): [number, number] {  if (    Array.isArray(explicitDomain) &&    explicitDomain.length === 2 &&    isFiniteNumber(explicitDomain[0]) &&    isFiniteNumber(explicitDomain[1]) &&    explicitDomain[0] <= explicitDomain[1]  ) {    return [explicitDomain[0], explicitDomain[1]]  }  const values: number[] = [0] // Include zero baseline for magnitude bars  for (const row of data) {    for (const key of seriesKeys) {      const val = row[key]      if (isFiniteNumber(val)) {        values.push(val)      }    }  }  const min = Math.min(...values)  const max = Math.max(...values)  if (min === max && min === 0) {    return [0, 10]  }  // 6% head room padding on positive extreme  const pad = (max - min) * 0.06 || 1  return [min < 0 ? min - pad : 0, max > 0 ? max + pad : 0]}export function defaultFormatValue(value: number): string {  if (!Number.isFinite(value)) return "—"  if (Number.isInteger(value)) return value.toLocaleString()  return value.toLocaleString(undefined, { maximumFractionDigits: 2 })}/* -------------------------------------------------------------------------- *//*  Interactive State Machine Reducer                                         *//* -------------------------------------------------------------------------- */export interface InteractiveBarState {  activeCategoryIndex: number | null  activeSeriesKey: string | null  lockedCategoryIndex: number | null  lockedSeriesKey: string | null  inputMode: InteractiveInputMode}export type InteractiveAction =  | { type: "POINTER_HOVER"; index: number; seriesKey?: string }  | { type: "POINTER_LEAVE" }  | { type: "TOGGLE_LOCK"; index: number; seriesKey?: string; inputMode: "touch" | "pointer" | "keyboard" }  | { type: "UNLOCK" }  | { type: "KEYBOARD_NAV"; index: number }  | { type: "RESET" }export const initialInteractiveState: InteractiveBarState = {  activeCategoryIndex: null,  activeSeriesKey: null,  lockedCategoryIndex: null,  lockedSeriesKey: null,  inputMode: null,}export function interactiveReducer(  state: InteractiveBarState,  action: InteractiveAction): InteractiveBarState {  switch (action.type) {    case "POINTER_HOVER": {      // If locked, pointer hover does not steal category focus, but can highlight series      if (state.lockedCategoryIndex !== null) {        return {          ...state,          activeSeriesKey: action.seriesKey ?? null,          inputMode: "pointer",        }      }      return {        ...state,        activeCategoryIndex: action.index,        activeSeriesKey: action.seriesKey ?? null,        inputMode: "pointer",      }    }    case "POINTER_LEAVE": {      // If locked, leave does not clear locked category      if (state.lockedCategoryIndex !== null) {        return {          ...state,          activeSeriesKey: null,        }      }      return {        ...state,        activeCategoryIndex: null,        activeSeriesKey: null,        inputMode: null,      }    }    case "TOGGLE_LOCK": {      // If already locked on this index, unlock      if (state.lockedCategoryIndex === action.index) {        return {          ...state,          lockedCategoryIndex: null,          lockedSeriesKey: null,          activeCategoryIndex: action.index,          activeSeriesKey: action.seriesKey ?? null,          inputMode: action.inputMode,        }      }      // Otherwise lock this category      return {        ...state,        lockedCategoryIndex: action.index,        lockedSeriesKey: action.seriesKey ?? null,        activeCategoryIndex: action.index,        activeSeriesKey: action.seriesKey ?? null,        inputMode: action.inputMode,      }    }    case "UNLOCK": {      return {        ...state,        lockedCategoryIndex: null,        lockedSeriesKey: null,      }    }    case "KEYBOARD_NAV": {      const isLocked = state.lockedCategoryIndex !== null      return {        ...state,        activeCategoryIndex: action.index,        activeSeriesKey: null,        lockedCategoryIndex: isLocked ? action.index : null,        inputMode: "keyboard",      }    }    case "RESET":      return initialInteractiveState    default:      return state  }}/* -------------------------------------------------------------------------- *//*  Main Component: InteractiveBars                                            *//* -------------------------------------------------------------------------- */const DEFAULT_SERIES_COLORS = [  "var(--chart-1, #3b82f6)",  "var(--chart-2, #f97316)",  "var(--chart-3, #10b981)",  "var(--chart-4, #a855f7)",  "var(--chart-5, #ec4899)",  "var(--chart-6, #eab308)",]export function InteractiveBars<TData extends Record<string, unknown> = Record<string, unknown>>({  data = [],  categoryKey,  series = [],  layout = "vertical",  height = 340,  domain = "auto",  showGrid = true,  showLegend = false,  interactiveLegend = false,  valueLabel = "none",  cursorMode = "band-and-bar",  lockOnTouch = true,  lockOnClick = true,  motion = true,  className,  title = "Interactive Categorical Comparison",  description = "Categorical bar chart with category band inspection, persistent touch lock, and keyboard traversal.",  loading = false,}: InteractiveBarsProps<TData>) {  const isHorizontal = layout === "horizontal"  const containerRef = React.useRef<HTMLElement>(null)  const chartRef = React.useRef<HTMLDivElement>(null)  // 1. Series visibility tracking (for optional interactive legend)  const [hiddenSeries, setHiddenSeries] = React.useState<Record<string, boolean>>({})  const visibleSeries = React.useMemo(() => {    return series.filter((s) => !hiddenSeries[s.key])  }, [series, hiddenSeries])  // 2. Interaction State  const [state, dispatch] = React.useReducer(interactiveReducer, initialInteractiveState)  // Live accessibility announcement state  const [liveAnnouncement, setLiveAnnouncement] = React.useState<string>("")  // 3. Immutably prepare categorical rows  const preparedData = React.useMemo<PreparedInteractiveDatum<TData>[]>(() => {    if (!Array.isArray(data)) return []    return data.map((item, idx) => {      const categoryVal = item[categoryKey]      const category = categoryVal != null ? String(categoryVal) : `Category ${idx + 1}`      const row: PreparedInteractiveDatum<TData> = {        ...item,        __source: item,        __index: idx,        __category: category,      }      // Ensure numeric values are normalized      for (const s of series) {        const val = item[s.key]        row[s.key] = isFiniteNumber(val) ? val : null      }      return row    })  }, [data, categoryKey, series])  // 4. Quantitative domain resolution  const computedDomain = React.useMemo(() => {    const visibleKeys = visibleSeries.map((s) => s.key)    return resolveInteractiveBarDomain(preparedData, visibleKeys, domain)  }, [preparedData, visibleSeries, domain])  // Check if any finite data exists  const hasFiniteData = React.useMemo(() => {    for (const row of preparedData) {      for (const s of visibleSeries) {        if (isFiniteNumber(row[s.key])) return true      }    }    return false  }, [preparedData, visibleSeries])  // Effective inspected index (locked takes precedence over active)  const inspectedIndex = state.lockedCategoryIndex !== null ? state.lockedCategoryIndex : state.activeCategoryIndex  const isLocked = state.lockedCategoryIndex !== null  const inspectedDatum = inspectedIndex !== null && inspectedIndex >= 0 && inspectedIndex < preparedData.length    ? preparedData[inspectedIndex]    : null  // 5. Single Tab-Stop Keyboard Navigation  const handleKeyDown = React.useCallback(    (e: React.KeyboardEvent) => {      if (!preparedData.length) return      const total = preparedData.length      const current = inspectedIndex !== null ? inspectedIndex : 0      if (isHorizontal) {        if (e.key === "ArrowDown") {          e.preventDefault()          const next = Math.min(total - 1, current + 1)          dispatch({ type: "KEYBOARD_NAV", index: next })          announceCategory(next)        } else if (e.key === "ArrowUp") {          e.preventDefault()          const prev = Math.max(0, current - 1)          dispatch({ type: "KEYBOARD_NAV", index: prev })          announceCategory(prev)        }      } else {        if (e.key === "ArrowRight") {          e.preventDefault()          const next = Math.min(total - 1, current + 1)          dispatch({ type: "KEYBOARD_NAV", index: next })          announceCategory(next)        } else if (e.key === "ArrowLeft") {          e.preventDefault()          const prev = Math.max(0, current - 1)          dispatch({ type: "KEYBOARD_NAV", index: prev })          announceCategory(prev)        }      }      if (e.key === "Home") {        e.preventDefault()        dispatch({ type: "KEYBOARD_NAV", index: 0 })        announceCategory(0)      } else if (e.key === "End") {        e.preventDefault()        dispatch({ type: "KEYBOARD_NAV", index: total - 1 })        announceCategory(total - 1)      } else if (e.key === "Enter" || e.key === " ") {        e.preventDefault()        const target = inspectedIndex !== null ? inspectedIndex : 0        dispatch({ type: "TOGGLE_LOCK", index: target, inputMode: "keyboard" })        const willLock = state.lockedCategoryIndex !== target        setLiveAnnouncement(`${preparedData[target].__category} ${willLock ? "locked" : "unlocked"}.`)      } else if (e.key === "Escape") {        e.preventDefault()        dispatch({ type: "UNLOCK" })        setLiveAnnouncement("Inspection unlocked.")      }    },    [preparedData, isHorizontal, inspectedIndex, state.lockedCategoryIndex]  )  const announceCategory = React.useCallback(    (idx: number) => {      if (!preparedData[idx]) return      const row = preparedData[idx]      const valuesSummary = visibleSeries        .map((s) => {          const val = row[s.key]          const fmt = s.valueFormatter || defaultFormatValue          return `${s.label}: ${isFiniteNumber(val) ? fmt(val) : "Unavailable"}`        })        .join(". ")      setLiveAnnouncement(`${row.__category}. Category ${idx + 1} of ${preparedData.length}. ${valuesSummary}`)    },    [preparedData, visibleSeries]  )  // Color resolver per series  const getSeriesColor = React.useCallback(    (s: InteractiveBarSeries<TData>, index: number) => {      return s.color || DEFAULT_SERIES_COLORS[index % DEFAULT_SERIES_COLORS.length]    },    []  )  // Series toggle helper  const toggleSeries = (key: string) => {    if (!interactiveLegend) return    setHiddenSeries((prev) => {      const next = { ...prev, [key]: !prev[key] }      // Don't allow hiding all series      const remaining = series.filter((s) => !next[s.key])      return remaining.length > 0 ? next : prev    })  }  // Click & tap handlers for the category band  const handleBandClick = (index: number) => {    if (!lockOnClick && !lockOnTouch) return    dispatch({ type: "TOGGLE_LOCK", index, inputMode: "pointer" })  }  if (loading) {    return <ChartLoadingState style={{ height }} className={className} />  }  if (preparedData.length === 0) {    return <ChartEmptyState style={{ height }} title="No Data Available" className={className} />  }  if (!hasFiniteData) {    return (      <ChartUnavailableState        style={{ height }}        title="No Numeric Observations"        description="None of the category records contain finite numbers."        className={className}      />    )  }  const chartMargin = isHorizontal    ? { top: 16, right: 32, bottom: 24, left: 24 }    : { top: 24, right: 24, bottom: 32, left: 24 }  return (    <figure      ref={containerRef}      role="region"      aria-label={title}      tabIndex={0}      onKeyDown={handleKeyDown}      className={cn(        "plotcn-chart plotcn-interactive-bars relative flex flex-col w-full focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 select-none",        className      )}    >      {/* Hidden Live Region for sparse accessibility announcements */}      <div aria-live="polite" aria-atomic="true" className="sr-only">        {liveAnnouncement}      </div>      {/* Screen reader summary & navigation guide */}      <div className="sr-only">        <h3>{title}</h3>        <p>{description}</p>        <p>          Showing {preparedData.length} categories across {visibleSeries.length} series.          Use {isHorizontal ? "Up and Down" : "Left and Right"} Arrow keys to inspect categories.          Press Enter or Space to toggle persistent inspection lock, and Escape to unlock.        </p>      </div>      {/* Optional Interactive or Static Legend */}      {showLegend && (        <div          role={interactiveLegend ? "group" : "region"}          aria-label="Series Legend"          className="flex flex-wrap items-center justify-end gap-4 px-3 py-2 text-xs text-muted-foreground border-b border-border/50 mb-2"        >          {series.map((s, idx) => {            const isHidden = Boolean(hiddenSeries[s.key])            const color = getSeriesColor(s, idx)            return (              <button                key={`legend-${String(s.key)}`}                type="button"                disabled={!interactiveLegend}                onClick={() => toggleSeries(String(s.key))}                className={cn(                  "flex items-center gap-1.5 transition-opacity duration-150",                  interactiveLegend ? "cursor-pointer hover:opacity-100" : "cursor-default",                  isHidden ? "opacity-35 line-through" : "opacity-90"                )}                aria-pressed={interactiveLegend ? !isHidden : undefined}                aria-label={`Toggle ${s.label} series`}              >                <span                  className="inline-block w-2.5 h-2.5 rounded-xs shrink-0"                  style={{ backgroundColor: color }}                  aria-hidden="true"                />                <span className="font-medium text-foreground">{s.label}</span>              </button>            )          })}        </div>      )}      {/* Main SVG Visualization Canvas */}      <div ref={chartRef} className="w-full relative" style={{ height: typeof height === "number" ? `${height}px` : height }}>        <ChartContainer className="w-full h-full relative">          <ResponsiveContainer            width="100%"            height="100%"            initialDimension={{              width: 320,              height: typeof height === "number" ? height : 340,            }}          >            <BarChart              data={preparedData}              layout={isHorizontal ? "vertical" : "horizontal"}              margin={chartMargin}              onMouseMove={(state) => {                if (state && state.activeTooltipIndex !== undefined) {                  const idx = Number(state.activeTooltipIndex)                  if (!Number.isNaN(idx)) {                    dispatch({ type: "POINTER_HOVER", index: idx })                  }                }              }}              onMouseLeave={() => {                dispatch({ type: "POINTER_LEAVE" })              }}              onClick={(state) => {                if (state && state.activeTooltipIndex !== undefined) {                  const idx = Number(state.activeTooltipIndex)                  if (!Number.isNaN(idx)) {                    handleBandClick(idx)                  }                }              }}            >              {showGrid && (                <CartesianGrid                  strokeDasharray="3 3"                  className="stroke-border/40"                  horizontal={!isHorizontal}                  vertical={isHorizontal}                />              )}              {isHorizontal ? (                <>                  <XAxis                    type="number"                    domain={computedDomain}                    tickLine={false}                    axisLine={{ stroke: "var(--border)", strokeWidth: 1 }}                    tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}                    tickFormatter={defaultFormatValue}                  />                  <YAxis                    type="category"                    dataKey="__category"                    tickLine={false}                    axisLine={false}                    tick={{ fill: "var(--foreground)", fontSize: 12, fontWeight: 500 }}                    width={90}                  />                </>              ) : (                <>                  <XAxis                    type="category"                    dataKey="__category"                    tickLine={false}                    axisLine={{ stroke: "var(--border)", strokeWidth: 1 }}                    tick={{ fill: "var(--foreground)", fontSize: 12, fontWeight: 500 }}                  />                  <YAxis                    type="number"                    domain={computedDomain}                    tickLine={false}                    axisLine={false}                    tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}                    tickFormatter={defaultFormatValue}                    width={50}                  />                </>              )}              {/* Native Category Band Cursor */}              <Tooltip                isAnimationActive={false}                cursor={{                  fill: isLocked ? "var(--chart-selection, #3b82f6)" : "var(--accent, #71717a)",                  opacity: isLocked ? 0.16 : 0.09,                  stroke: isLocked ? "var(--chart-selection, #3b82f6)" : "none",                  strokeWidth: isLocked ? 1.5 : 0,                  strokeDasharray: isLocked ? "4 2" : undefined,                  rx: 4,                  ry: 4,                }}                allowEscapeViewBox={{ x: false, y: false }}                content={({ active, payload }) => {                  // If locked or hovered, resolve datum                  const datum = inspectedDatum || (active && payload && payload.length ? (payload[0].payload as PreparedInteractiveDatum<TData>) : null)                  if (!datum) return null                  const isCurrentlyLocked = state.lockedCategoryIndex === datum.__index                  return (                    <div                      role="tooltip"                      className="rounded-xl border border-border/80 bg-popover/95 backdrop-blur-md px-3.5 py-3 shadow-2xl text-xs space-y-2 pointer-events-none min-w-[210px] max-w-[calc(100cqw-16px)]"                    >                      {/* Header: Category + Locked Pill */}                      <div className="flex items-center justify-between gap-2 border-b border-border/50 pb-2">                        <span className="font-semibold text-foreground text-sm truncate">                          {datum.__category}                        </span>                        {isCurrentlyLocked ? (                          <span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 border border-emerald-500/30 px-2 py-0.5 text-[10px] font-mono text-emerald-400 font-medium shrink-0">                            <HugeiconsIcon icon={LockIcon} size={10} />                            <span>Locked</span>                          </span>                        ) : (                          <span className="text-[10px] font-mono text-muted-foreground/80 shrink-0">                            Category {datum.__index + 1}                          </span>                        )}                      </div>                      {/* Series Breakdown */}                      <div className="space-y-1.5 pt-0.5">                        {visibleSeries.map((s, idx) => {                          const val = datum[s.key]                          const hasVal = isFiniteNumber(val)                          const color = getSeriesColor(s, idx)                          const isHighlighted = cursorMode === "band-and-bar" && state.activeSeriesKey === s.key                          const fmt = s.valueFormatter || defaultFormatValue                          return (                            <div                              key={`tooltip-${String(s.key)}`}                              className={cn(                                "flex items-center justify-between gap-3 px-1 py-0.5 rounded transition-colors",                                isHighlighted ? "bg-white/10 font-semibold text-foreground" : "text-muted-foreground"                              )}                            >                              <div className="flex items-center gap-1.5 min-w-0">                                <span                                  className="w-2.5 h-2.5 rounded-xs shrink-0"                                  style={{ backgroundColor: color }}                                  aria-hidden="true"                                />                                <span className={cn("truncate", isHighlighted ? "text-foreground" : "text-foreground/90")}>                                  {s.label}                                </span>                              </div>                              <span className="font-mono text-foreground font-medium shrink-0">                                {hasVal ? fmt(val as number) : "Unavailable"}                              </span>                            </div>                          )                        })}                      </div>                      {/* Interaction Footer Instruction */}                      <div className="border-t border-border/40 pt-1.5 text-[10px] font-mono text-muted-foreground/70 flex items-center justify-between">                        <span>{isCurrentlyLocked ? "Press Esc to unlock" : "Click / tap to lock"}</span>                        <span>{isCurrentlyLocked ? <HugeiconsIcon icon={LockOpenIcon} size={10} /> : <HugeiconsIcon icon={LockIcon} size={10} />}</span>                      </div>                    </div>                  )                }}              />              {/* Grouped Bar Series */}              {visibleSeries.map((s, idx) => {                const color = getSeriesColor(s, idx)                const isSeriesActive = state.activeSeriesKey === s.key                return (                  <Bar                    key={`bar-series-${String(s.key)}`}                    dataKey={s.key as string}                    name={s.label}                    fill={color}                    fillOpacity={                      cursorMode === "band-and-bar" && state.activeSeriesKey && !isSeriesActive                        ? 0.5                        : 1                    }                    stroke={isSeriesActive ? "var(--foreground)" : "none"}                    strokeWidth={isSeriesActive ? 1.5 : 0}                    radius={isHorizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]}                    isAnimationActive={Boolean(motion)}                    animationDuration={typeof motion === "object" && motion.duration ? motion.duration : 500}                    onMouseEnter={() => {                      dispatch({ type: "POINTER_HOVER", index: inspectedIndex ?? 0, seriesKey: String(s.key) })                    }}                    onMouseLeave={() => {                      dispatch({ type: "POINTER_HOVER", index: inspectedIndex ?? 0 })                    }}                  >                    {valueLabel === "auto" && (                      <LabelList                        dataKey={s.key as string}                        position={isHorizontal ? "right" : "top"}                        formatter={(val: unknown) => (isFiniteNumber(val) ? defaultFormatValue(val) : "")}                        className="fill-foreground font-mono text-[10px]"                      />                    )}                  </Bar>                )              })}            </BarChart>          </ResponsiveContainer>        </ChartContainer>      </div>      {/* Accessible Structured Data Fallback Table */}      <div className="sr-only">        <table>          <caption>{title} data table</caption>          <thead>            <tr>              <th scope="col">{categoryKey}</th>              {visibleSeries.map((s) => (                <th key={`th-${String(s.key)}`} scope="col">                  {s.label}                </th>              ))}            </tr>          </thead>          <tbody>            {preparedData.map((row) => (              <tr key={`tr-${String(row.__category)}`}>                <th scope="row">{row.__category}</th>                {visibleSeries.map((s) => {                  const val = row[s.key]                  const fmt = s.valueFormatter || defaultFormatValue                  return <td key={`td-${String(row.__category)}-${String(s.key)}`}>{isFiniteNumber(val) ? fmt(val) : "Unavailable"}</td>                })}              </tr>            ))}          </tbody>        </table>      </div>    </figure>  )}