014 / RECHARTS / AREA

Range Area

Recharts

Bounded area visualization for showing a lower/upper envelope across an ordered domain, with optional central trend, truthful interval semantics, and explicit missing-bound behavior.

SPEC
#014
ENGINE
Recharts
FAMILY
Area
RENDERER
svg
STATUS
preview

Installation

PLOTCN/REGISTRY/AREA-RANGE/SOURCE
pnpm dlx shadcn@latest add @plotcn/area-range

Checking public registry…

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

Copied as source into your project (requires recharts).

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

Overview

Range Area is the envelope-specialized chart of the Plotcn Area family. It visualizes a bounded lower and upper range across an ordered domain, with an optional central trend line.

The primary analytical question answered by Range Area is:

"What range or envelope contains the values across the ordered domain?"

The optional secondary question is:

"Where does the central or expected value sit inside that range?"

Unlike regular area charts that fill between a single metric and a zero baseline, Range Area fills strictly between two supplied boundary values (lower(x) → upper(x)). The filled region is the quantitative data itself—not a decorative background.

TSX
import { RangeArea } from "@/components/charts/recharts/area-range"const latencyData = [  { time: "10:00", min: 18, max: 26, median: 22 },  { time: "11:00", min: 19, max: 28, median: 23 },  { time: "12:00", min: 20, max: 36, median: 28 },  { time: "13:00", min: 17, max: 29, median: 21 },  { time: "14:00", min: 18, max: 25, median: 21 },  { time: "15:00", min: 21, max: 32, median: 25 },]export function ApiLatencyEnvelope() {  return (    <RangeArea      data={latencyData}      xKey="time"      series={{        lowerKey: "min",        upperKey: "max",        valueKey: "median",        label: "Latency envelope",        lowerLabel: "Min latency",        upperLabel: "Max latency",        valueLabel: "Median latency",      }}      color="var(--chart-1)"      valueColor="var(--chart-2)"      fillOpacity={0.25}      showGrid    />  )}

Area-Family Positioning

The Plotcn Area family provides four distinct analytical instruments:

Consideration Prism Area (011) Stack Flow Area (012) Percent Stream Area (013) Range Area (014)
ConsiderationPrism Area (011)Stack Flow Area (012)Percent Stream Area (013)Range Area (014)
Primary Question"How much magnitude exists relative to baseline?""How do parts and total magnitude combine?""How does the composition of the whole evolve?""What envelope or interval contains the observations?"
Fill GeometryFills between metric and baselineFills additive layers from bottom to topFills 100% normalized proportional sharesFills between supplied lower and upper bounds
Baseline RoleConfigurable baseline (y=0y = 0 or domain min)Fixed baseline (y=0y = 0)Fixed baseline (0%0\%)No baseline fill; lower bound defines floor
Primary IdentitySingle continuous areaStacked additive layers100% normalized streamBounded range envelope; centerline is optional
CenterlineNot applicableNot applicableNot applicableSupported via optional valueKey

Critical Analytical Distinctions

  1. Range Area vs Range Line (004):
    • Range Line: The central line is the primary visual and analytical object; the envelope is secondary supporting context.
    • Range Area: The bounded envelope itself is the primary visual and analytical object; the centerline is optional supporting context.
  2. Range Area vs Forecast Line (010):
    • Forecast Line: Implements an explicit transition from observed historical records to predicted future values.
    • Range Area: Displays an envelope across the entire ordered domain with no implied observation-versus-forecast transition.
  3. Range Area vs Threshold Area:
    • Threshold Area: Contextual reference limits are static horizontal lines.
    • Range Area: Lower and upper bounds vary dynamically at every observation coordinate across the domain.

Range Envelope Model

INTERVAL ARCHITECTUREBounded Envelope + Optional Centerline

Range Area Fills Between Two Supplied Bounds (lower → upper)

Bounded Range
Centerline (optional)
40 ms30 ms20 ms10 msupperKey (Max)valueKey (Median)lowerKey (Min)Span: 16 ms12:00 UTC · InspectedRange: 20 – 36 msMin: 20 ms · Max: 36 msMedian: 28 ms10:0011:0012:0013:0014:0015:00
Core Invariant: The band only fills between lower(x) and upper(x); zero is never an implicit baseline.
Centerline is independently supplied; never clamped or midpoint-derived

1. Bounded Fill Geometry

At every domain coordinate xx:

Envelope(x)=[lower(x),upper(x)]\text{Envelope}(x) = [\text{lower}(x), \text{upper}(x)]

The SVG area polygon fills the vertical span between lower(x)\text{lower}(x) and upper(x)\text{upper}(x). Neither zero nor the chart canvas bottom acts as an implicit baseline.

2. Optional Centerline

When valueKey is provided in series, a solid signal line renders over the envelope.

  • No Midpoint Synthesis: If valueKey is omitted, Plotcn never invents a midpoint ((lower+upper)/2(\text{lower} + \text{upper}) / 2). The mathematical midpoint may not represent any real statistic.
  • No Range Clamping: If the central value falls outside the envelope (e.g. an observed metric breaching an SLA tolerance band), Plotcn never clamps the centerline into the band. Values are rendered truthfully.

Truthful State Matrix & Missing Bounds

INTERVAL VALIDITY MATRIXNo Data Fabrication

Truthful Handling of Missing Bounds, Missing Centerlines, and Inverted Data

1. Valid IntervalComplete

lower ≤ upper and centerline present. Full bounded envelope with center marker.

low: 18 · up: 26 · center: 22
2. Missing BoundBand Gaps

One or both bounds null. Band gaps cleanly; centerline continues independently.

gap in envelope
low: null · up: 26 · center: 22
3. Missing CenterCenter Gaps

Centerline null while bounds valid. Band stays continuous; centerline gaps.

— gap —
low: 18 · up: 26 · center: null
4. Invalid (low > up)Never Swapped

Inverted bounds omitted from band. Never silently swapped or rewritten.

invalid: low > up
low: 28 · up: 19 (omitted)
Guaranteed Safety: Lower and upper bounds are never swapped; missing bounds never become zero.
Independent channel validation guarantees truthful reporting

Range Area handles missing bounds and malformed intervals with strict quantitative integrity:

Observation Condition Range Band Behavior Centerline Behavior Tooltip Range Status Tooltip Center Status
Observation ConditionRange Band BehaviorCenterline BehaviorTooltip Range StatusTooltip Center Status
Valid Interval & CenterFull filled bandRendered point / lineFormatted lower – upperFormatted value
Missing Lower BoundBand gapsRendered if valid"Unavailable"Formatted value
Missing Upper BoundBand gapsRendered if valid"Unavailable"Formatted value
Both Bounds MissingBand gapsRendered if valid"Unavailable"Formatted value
Missing CenterlineFull filled bandCenterline gapsFormatted lower – upper"Unavailable"
Equal Bounds (lower=upperlower = upper)Zero-width line/pointRendered if validFormatted value (Zero width)Formatted value
Inverted Bounds (lower>upperlower > upper)Band segment omittedRendered if valid"Invalid bounds"Formatted value
[!CAUTION] Data Integrity Guarantee: If lower>upperlower > upper, Plotcn never silently swaps the bounds. Silent swapping falsifies user data. Instead, the invalid segment is omitted from the envelope, an "Invalid bounds" badge appears in inspection tooltips, and a developmental warning is logged.

Curve Safety & Smoothing Invariants

Upper and lower envelope boundaries and the optional centerline must share the exact same curve interpolation algorithm:

TSX
<RangeArea  data={data}  xKey="time"  series={series}  curve="linear" // "linear" | "monotone" | "step"/>
  • Default is linear: For interval envelopes, piecewise linear interpolation guarantees that if lower(x)upper(x)lower(x) \le upper(x) at every point, the interpolated band will never self-intersect or invert between points.
  • monotone Safety: When using "monotone", ensure sample points are sufficiently dense to prevent cubic spline overshoot near sudden inflection steps.
  • Equal Curves Mandatory: Plotcn prohibits configuring different curves for lower and upper boundaries, which would create impossible synthetic self-intersections.

Data Contract

Range Area accepts a readonly array of objects. Caller data is never mutated or sorted in place.

TypeScript
interface LatencyObservation {  time: string  min: number | null  max: number | null  median?: number | null}
TSX
<RangeArea  data={observations}  xKey="time"  series={{    lowerKey: "min",    upperKey: "max",    valueKey: "median",    label: "Operating envelope",  }}/>

Negative & Cross-Zero Bounds

Unlike additive stacked area charts, Range Area naturally supports negative numbers and cross-zero intervals:

  • Negative Envelopes: E.g. temperature anomalies spanning [12C,4C][-12^\circ\text{C}, -4^\circ\text{C}].
  • Zero-Crossing Envelopes: E.g. cash flow variance spanning [5k,+15k][-5\text{k}, +15\text{k}].
  • Zero Is Valid: Observations with $0$ are treated as truthful quantitative coordinates, not missing values.

Color Customization & Theme Roles

Range Area accepts semantic CSS theme tokens or custom color overrides:

TSX
<RangeArea  data={data}  xKey="time"  series={series}  color="var(--chart-1)"        // Envelope fill & boundary strokes  valueColor="var(--chart-2)"   // Optional centerline  fillOpacity={0.25}            // Fill translucency (0.05 to 1.0)  selectionColor="var(--chart-selection)"/>
  • Distinguishability: If color === valueColor, the envelope and centerline remain readily distinguishable via fill translucency, line weight, and geometry.
  • Theme Adaptation: Semantic tokens adapt automatically across Light and Dark themes.

Props Reference

Property Type Default Description
PropertyTypeDefaultDescription
datareadonly TData[][]Readonly array of observation records. Caller data is never mutated.
xKeykeyof TData & stringProperty key for horizontal domain coordinates.
seriesRangeAreaSeries<TData>Semantic configuration defining lowerKey, upperKey, optional valueKey, and labels.
heightnumber | string320Container height in pixels or CSS units.
curve"linear" | "monotone" | "step""linear"Shared interpolation curve applied identically to envelope and centerline.
domain[number, number] | "auto"autoExplicit vertical domain override. Defaults to auto-padded scale enclosing all bounds.
colorstringvar(--chart-1)Primary color for range envelope fill and boundaries.
valueColorstringvar(--chart-2)Color for optional central signal line.
fillOpacitynumber0.25Translucency of the range envelope fill (0.05 to 1.0).
selectionColorstringvar(--chart-selection)Accent color for active crosshair and locked pin.
showGridbooleantrueWhether to render subtle Cartesian grid reference lines.
showXAxisbooleantrueWhether to render horizontal domain labels.
showYAxisbooleantrueWhether to render vertical metric scale ticks.
showLegendbooleanfalseWhether to render series identity legend.
lockableTooltipbooleantrueEnables persistent tooltip pinning on click, tap, or Enter/Space.
motionboolean | { duration?: number }trueControls entry animations. Automatically bypassed under reduced motion.
valueFormatter(value: number) => stringn.toLocaleString()Formatter for bounds and metric values.
xFormatter(value: string | number) => stringFormatter for horizontal tick labels.
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):
<RangeArea
  data={data}
  xKey="date"
  series={{
    lowerKey: "min",
    upperKey: "max",
    valueKey: "median",
    label: "Latency envelope",
  }}
/>
Interactive Prop Preview Lab
curve"linear" | "monotone" | "step"

Curve interpolation algorithm shared identically across the envelope and centerline.

Select value to preview live:
Active: curve="linear"Default: "linear"
colorstring

Primary color for range envelope fill and boundary strokes.

Select value to preview live:var(--chart-1)
Active: color="var(--chart-1)"Default: "var(--chart-1)"
fillOpacitynumber

Opacity of the range envelope fill (0.05 to 1.0).

Select value to preview live:
Active: fillOpacity={0.25}Default: 0.25
showGridboolean

Whether to render subtle horizontal Cartesian grid reference lines.

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

Whether to render the series identity legend.

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

Enables persistent tooltip pinning on click, tap, or Enter/Space.

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

Readonly array of observation records. Caller data is never mutated or sorted in place.

xKeyReq
keyof TData & stringYes

Property name on data records for horizontal domain coordinates.

RangeAreaSeries<TData>Yes

Semantic configuration defining lowerKey, upperKey, optional valueKey, and display labels.

number | string320No

Container height in pixels or standard CSS dimension strings.

"linear" | "monotone" | "step""linear"No

Curve interpolation algorithm shared identically across the envelope and centerline.

string"var(--chart-1)"No

Primary color for range envelope fill and boundary strokes.

string"var(--chart-2)"No

Color for optional central signal line and center marker.

number0.25No

Opacity of the range envelope fill (0.05 to 1.0).

string"var(--chart-selection)"No

Accent color for the active inspection crosshair and locked pin.

booleantrueNo

Whether to render subtle horizontal Cartesian grid reference lines.

booleantrueNo

Whether to render the horizontal category scale.

booleantrueNo

Whether to render the vertical value axis.

booleanfalseNo

Whether to render the series identity legend.

booleantrueNo

Enables persistent tooltip pinning on click, tap, or Enter/Space.

"draw" | "fade" | "none""draw"No

Initial reveal animation style. Bypassed automatically under reduced motion.

(value: number) => stringNo

Custom formatter for range bounds and centerline values in tooltips.

(value: string | number) => stringNo

Custom formatter for horizontal domain tick labels.

string"Range Area Chart"No

Accessible heading announced to screen readers.

stringundefinedNo

Accessible descriptive summary announced to screen readers.

04 / Cookbook & States

Component Variants & Edge States

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

API Latency Envelope with Median

Hourly observed min/max latency bounds with median signal line illustrating typical service operating conditions.

<RangeArea
  data={latencyData}
  xKey="time"
  series={{
    lowerKey: "min",
    upperKey: "max",
    valueKey: "median",
    label: "Latency envelope",
    lowerLabel: "Min latency",
    upperLabel: "Max latency",
    valueLabel: "Median latency",
  }}
  color="var(--chart-1)"
  valueColor="var(--chart-2)"
  fillOpacity={0.25}
  showGrid
/>

Tolerance Envelope Without Centerline

Pure lower/upper operating envelope demonstrating that a centerline is never midpoint-derived when valueKey is omitted.

<RangeArea
  data={latencyData}
  xKey="time"
  series={{
    lowerKey: "min",
    upperKey: "max",
    label: "Operating tolerance",
    lowerLabel: "Floor",
    upperLabel: "Ceiling",
  }}
  color="var(--chart-1)"
  fillOpacity={0.3}
  showGrid
/>
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

Range Area preserves full lower/upper envelope bounds, active inspection slice, and optional centerline across all device widths down to 320px.

Desktop
>= 1024px

Spacious envelope band, full domain ticks, and multi-line inspection tooltip displaying range span and centerline.

Tablet
640px - 1023px

Adaptive domain tick thinning, preserved interval geometry, and synchronized nearest-X inspection.

Mobile
< 640px

Compact gutters, full interval band without dropping bounds, and compact locked tooltip preventing horizontal page overflow.

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

Accessibility & Navigation Standards

Single keyboard tab stop on root figure with ArrowLeft, ArrowRight, Home, End, Enter/Space, and Escape shortcuts.

Semantic Role & Landmark

Container mounts as region with explicit assistive label.

Color-Independent Legibility

Envelope boundary strokes, distinct fill translucency, formatted tooltip readouts, and off-screen structured data table provide accessible non-color differentiation.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “Announces domain coordinate, range envelope bounds (lower to upper), and centerline value when present. Explicitly alerts if bounds are missing or invalid.

Reduced Motion Support

All entrance animations immediately bypass when prefers-reduced-motion is detected in user system preferences.

Keyboard Interaction Model
Keyboard interaction model
KeyAction
ArrowRightInspect next chronological observation across the domain.
ArrowLeftInspect previous chronological observation across the domain.
HomeJump inspection directly to the first observation.
EndJump inspection directly to the final observation.
Enter / SpaceLock or unlock persistent inspection at the active coordinate.
EscapeRelease locked selection and dismiss active inspection tooltip.

Data Safety Guarantees

  1. Supplied Bounds Only: Plotcn fills only between supplied lowerKey and upperKey values.
  2. No Midpoint Synthesis: Never invents a central line if valueKey is omitted.
  3. No Bound Swapping: If lower>upperlower > upper, data is flagged invalid and omitted from the band; bounds are never reversed.
  4. No Zero Substitution: Missing bounds (nullnull) result in a gap; zero is never substituted.
  5. No Clamping: Centerlines outside the envelope are displayed truthfully without artificial clamping.
  6. Caller Immutability: Original data records are never mutated or sorted in place.
  7. Accessibility Disclosure: Structured HTML data table disclosure provides full non-visual access to all interval coordinates.
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
RangeArea(Root figure element with keyboard navigation and ARIA accessibility shell)
├──ResponsiveContainer[Responsive container wrapper]

Handles container dimension measurement and SVG viewBox sizing

└──ComposedChart[Recharts Cartesian SVG coordinator]

Coordinates coordinate scales, Cartesian grid, Area range envelope, and Line centerline

Involved Source Files & Registry Assets
registry/recharts/area-range.tsx
Complete Range Area component with bounded envelope geometry, truthful missing-data policies, and interval safety
registry/recharts/area-range.tsx
"use client"import * as React from "react"import {  ResponsiveContainer,  ComposedChart,  Area,  Line,  XAxis,  YAxis,  Tooltip,  CartesianGrid,  ReferenceLine,} from "recharts"import { HugeiconsIcon } from "@hugeicons/react"import { LockKeyIcon } from "@hugeicons/core-free-icons"import { useChartReducedMotion } from "../shared/use-chart-reduced-motion"import {  ChartLoadingState,  ChartEmptyState,  ChartErrorState,  ChartUnavailableState,} from "../shared/chart-state"import { ChartContainer } from "../shared/chart-container"import { cn } from "@/lib/utils"/* -------------------------------------------------------------------------- *//*  Type Definitions                                                          *//* -------------------------------------------------------------------------- */export type NumericKeyOf<TData> = [keyof TData] extends [never]  ? string  : {      [K in keyof TData]: TData[K] extends number | null | undefined ? K : never    }[keyof TData] extends never  ? string  : {      [K in keyof TData]: TData[K] extends number | null | undefined ? K : never    }[keyof TData] & stringexport type RangeAreaCurve = "linear" | "monotone" | "step"export interface RangeAreaSeries<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Key for lower bound of the interval */  lowerKey: NumericKeyOf<TData>  /** Key for upper bound of the interval */  upperKey: NumericKeyOf<TData>  /** Optional key for central or expected value */  valueKey?: NumericKeyOf<TData>  /** Human-readable display label for the envelope (e.g. "Latency Range", "Capacity Band") */  label: string  /** Optional label for the lower bound (e.g. "Min Latency", "Floor") */  lowerLabel?: string  /** Optional label for the upper bound (e.g. "Max Latency", "Ceiling") */  upperLabel?: string  /** Optional label for the centerline (e.g. "Median Latency", "Expected") */  valueLabel?: string  /** Optional custom formatter for metric values */  valueFormatter?: (value: number) => string}export type RangeIntervalState = "valid" | "missing" | "invalid" | "zero-width"export type CenterValueState = "valid" | "missing"export interface NormalizedRangeAreaDatum<TData = any> {  __x: string | number  __index: number  __lower: number | null  __upper: number | null  __value: number | null  __range: [number, number] | null  __rangeState: RangeIntervalState  __valueState: CenterValueState  __raw: TData}export interface RangeAreaActiveDatum<  TData extends Record<string, unknown> = Record<string, unknown>,  XVal extends string | number = string | number> {  index: number  x: XVal  raw: TData  lower: number | null  upper: number | null  value: number | null  rangeState: RangeIntervalState  valueState: CenterValueState  isLocked: boolean}export interface RangeAreaProps<TData extends Record<string, unknown> = Record<string, unknown>> {  /**   * Readonly array of observation records. Caller data is never mutated or sorted in place.   */  data: readonly TData[]  /**   * Property name on data records for horizontal domain coordinates.   */  xKey: keyof TData & string  /**   * Semantic series configuration defining lower, upper, and optional centerline keys.   */  series: RangeAreaSeries<TData>  /**   * Container height in pixels or standard CSS dimension string.   * Default: 320   */  height?: number | string  /**   * Shared curve interpolation algorithm applied identically to the envelope and centerline.   * Default: "linear" (guarantees truthful boundary ordering and prevents overshoot).   */  curve?: RangeAreaCurve  /**   * Explicit vertical domain override. Defaults to auto-computed domain enclosing all valid bounds and centerline.   */  domain?: [number, number] | ["auto", "auto"] | "auto"  /**   * Primary color for the range envelope fill and boundary strokes.   * Default: "var(--chart-1)"   */  color?: string  /**   * Secondary color for the optional central signal line.   * Default: "var(--chart-2)"   */  valueColor?: string  /**   * Opacity of the range envelope fill (0.05 to 1.0).   * Default: 0.25   */  fillOpacity?: number  /**   * Accent color for active inspection crosshair and locked selection pin.   * Default: "var(--chart-selection)"   */  selectionColor?: string  /**   * Whether to render subtle horizontal Cartesian grid reference lines.   * Default: true   */  showGrid?: boolean  /**   * Whether to render the horizontal domain axis.   * Default: true   */  showXAxis?: boolean  /**   * Whether to render the vertical value axis.   * Default: true   */  showYAxis?: boolean  /**   * Whether to render the series identity legend.   * Default: false   */  showLegend?: boolean  /**   * Whether clicking or pressing Enter/Space locks active inspection at the selected X coordinate.   * Default: true   */  lockableTooltip?: boolean  /**   * Controls entry and update animations.   * Default: true   */  motion?: boolean | { duration?: number }  /**   * Reveal animation mode ("draw", "fade", "none").   * Default: "draw"   */  animation?: "draw" | "fade" | "none"  /**   * Formatter for horizontal domain tick labels.   */  xFormatter?: (val: string | number) => string  /**   * Default value formatter for range bounds and centerline values in tooltips.   */  valueFormatter?: (val: number) => string  /**   * Accessible heading announced to screen readers.   * Default: "Range Area Chart"   */  title?: string  /**   * Accessible descriptive summary announced to screen readers.   */  description?: string  /**   * Loading state indicator.   */  loading?: boolean  /**   * Unavailable state indicator.   */  unavailable?: boolean | string  /**   * Error state indicator or Error object.   */  error?: Error | string | null  /**   * Callback invoked whenever active inspection coordinate changes.   */  onActiveDatumChange?: (datum: RangeAreaActiveDatum<TData> | null) => void  /**   * Additional CSS classes applied to root figure element.   */  className?: string}/* -------------------------------------------------------------------------- *//*  Pure Validation & Domain Algorithms                                       *//* -------------------------------------------------------------------------- */export function isFiniteNumber(val: unknown): val is number {  return typeof val === "number" && Number.isFinite(val)}/** * Normalizes input observations with strict interval validation: * 1. Validates that lower <= upper. If lower > upper, interval is treated as invalid (omitted, no silent swapping). * 2. Missing bounds (null / undefined): Band gaps truthfully without fabricating zero or domain min. * 3. Equal bounds (lower === upper): Valid zero-width interval (rendered as line/point). * 4. Centerline independence: Valid centerline still renders even when range is missing or invalid. * 5. Centerline is NEVER clamped into the range band. * 6. Supports negative and cross-zero intervals naturally. * 7. Never mutates caller array or objects. */export function normalizeRangeAreaData<TData extends Record<string, unknown>>(  data: readonly TData[],  xKey: keyof TData & string,  series: RangeAreaSeries<TData>): {  rows: NormalizedRangeAreaDatum<TData>[]  hasInvalidIntervals: boolean  invalidDetails?: string} {  if (!Array.isArray(data) || data.length === 0) {    return { rows: [], hasInvalidIntervals: false }  }  const rows: NormalizedRangeAreaDatum<TData>[] = []  let hasInvalidIntervals = false  let firstInvalidDetail: string | undefined  for (let i = 0; i < data.length; i++) {    const d = data[i]    const rawX = d[xKey]    const xVal = typeof rawX === "string" || typeof rawX === "number" ? rawX : String(rawX ?? "")    const rawL = d[series.lowerKey]    const rawU = d[series.upperKey]    const rawV = series.valueKey ? d[series.valueKey] : undefined    const low = isFiniteNumber(rawL) ? rawL : null    const up = isFiniteNumber(rawU) ? rawU : null    const val = isFiniteNumber(rawV) ? rawV : null    let rangeTuple: [number, number] | null = null    let rangeState: RangeIntervalState = "missing"    if (low !== null && up !== null) {      if (low < up) {        rangeTuple = [low, up]        rangeState = "valid"      } else if (low === up) {        rangeTuple = [low, up]        rangeState = "zero-width"      } else {        // low > up: invalid data! Never silently swap.        rangeTuple = null        rangeState = "invalid"        hasInvalidIntervals = true        if (!firstInvalidDetail) {          firstInvalidDetail = `Invalid bound at "${xVal}": lower (${low}) exceeds upper (${up}).`        }      }    } else {      rangeTuple = null      rangeState = "missing"    }    const valueState: CenterValueState = val !== null ? "valid" : "missing"    rows.push({      __x: xVal,      __index: i,      __lower: low,      __upper: up,      __value: val,      __range: rangeTuple,      __rangeState: rangeState,      __valueState: valueState,      __raw: d,    })  }  return {    rows,    hasInvalidIntervals,    invalidDetails: firstInvalidDetail,  }}/** * Computes an honest Cartesian Y-domain enclosing all valid lower bounds, * upper bounds, and centerline values with protective safety padding. */export function calculateRangeAreaDomain(  rows: readonly NormalizedRangeAreaDatum[],  explicitDomain?: [number, number] | ["auto", "auto"] | "auto"): [number, number] {  if (    Array.isArray(explicitDomain) &&    typeof explicitDomain[0] === "number" &&    typeof explicitDomain[1] === "number" &&    Number.isFinite(explicitDomain[0]) &&    Number.isFinite(explicitDomain[1])  ) {    return [explicitDomain[0], explicitDomain[1]]  }  let min = Infinity  let max = -Infinity  for (const r of rows) {    if (r.__lower !== null && r.__upper !== null && r.__lower <= r.__upper) {      if (r.__lower < min) min = r.__lower      if (r.__upper > max) max = r.__upper    } else {      if (r.__lower !== null) {        if (r.__lower < min) min = r.__lower        if (r.__lower > max) max = r.__lower      }      if (r.__upper !== null) {        if (r.__upper < min) min = r.__upper        if (r.__upper > max) max = r.__upper      }    }    if (r.__value !== null) {      if (r.__value < min) min = r.__value      if (r.__value > max) max = r.__value    }  }  if (!Number.isFinite(min) || !Number.isFinite(max)) {    return [0, 100]  }  if (min === max) {    const delta = Math.abs(min) * 0.1 || 1    return [min - delta, max + delta]  }  const span = max - min  const padding = span * 0.08  return [min - padding, max + padding]}/* -------------------------------------------------------------------------- *//*  Custom Locked Inspection Tooltip Content                                  *//* -------------------------------------------------------------------------- */interface RangeAreaTooltipProps<TData extends Record<string, unknown>> {  activeDatum: RangeAreaActiveDatum<TData> | null  series: RangeAreaSeries<TData>  color: string  valueColor: string  valueFormatter: (val: number) => string  onUnlock?: () => void  isCompact?: boolean}function RangeAreaTooltipContent<TData extends Record<string, unknown>>({  activeDatum,  series,  color,  valueColor,  valueFormatter,  onUnlock,  isCompact = false,}: RangeAreaTooltipProps<TData>) {  if (!activeDatum) return null  const { lower, upper, value, rangeState, valueState, isLocked, x } = activeDatum  return (    <div      role="tooltip"      aria-hidden="false"      className={cn(        "plotcn-interactive-tooltip relative rounded-lg border border-white/[0.14] bg-zinc-950/95 font-sans text-zinc-200 shadow-2xl backdrop-blur-md transition-all duration-150 select-none",        isCompact          ? "min-w-[120px] max-w-[190px] p-2 text-[10px]"          : "min-w-[200px] max-w-[280px] p-3 text-xs"      )}    >      {/* Tooltip Header */}      <div        className={cn(          "tooltip-header flex items-center justify-between gap-2 border-b border-white/[0.08]",          isCompact ? "pb-1 mb-1.5" : "pb-1.5 mb-2"        )}      >        <span          className={cn(            "font-mono font-semibold text-zinc-300 truncate",            isCompact ? "text-[10px]" : "text-[11px]"          )}        >          {String(x)}        </span>        {isLocked && (          <button            type="button"            onClick={onUnlock}            title="Click to release locked selection"            className={cn(              "flex items-center gap-1 rounded font-medium text-amber-400 bg-amber-500/10 border border-amber-500/20 hover:bg-amber-500/20 transition-colors shrink-0",              isCompact ? "px-1 py-0.2 text-[8px]" : "px-1.5 py-0.5 text-[10px]"            )}          >            <HugeiconsIcon icon={LockKeyIcon} size={isCompact ? 8 : 10} />            <span>Locked</span>          </button>        )}      </div>      {/* Range Envelope Readout */}      <div className={isCompact ? "space-y-1" : "space-y-2"}>        <div className="flex flex-col gap-0.5">          <div            className={cn(              "tooltip-row flex items-center justify-between gap-2",              isCompact ? "text-[10px]" : "text-[11px] text-zinc-400"            )}          >            <span className="flex items-center gap-1 font-medium text-zinc-300 truncate">              <span                className={cn(                  "rounded-xs border border-white/20 shrink-0",                  isCompact ? "size-2" : "size-2.5"                )}                style={{ backgroundColor: color }}              />              <span className="truncate">{series.label}</span>            </span>            {rangeState === "valid" && lower !== null && upper !== null && (              <span className="font-mono font-medium text-zinc-200 shrink-0">                {valueFormatter(lower)}{valueFormatter(upper)}              </span>            )}            {rangeState === "zero-width" && lower !== null && (              <span className="font-mono font-medium text-zinc-200 shrink-0">                {valueFormatter(lower)}              </span>            )}            {rangeState === "missing" && (              <span                className={cn(                  "rounded bg-zinc-800/60 font-mono text-zinc-500 shrink-0",                  isCompact ? "px-1 py-0 text-[8px]" : "px-1 py-0.5 text-[10px]"                )}              >                Unavailable              </span>            )}            {rangeState === "invalid" && (              <span                className={cn(                  "rounded bg-rose-500/10 border border-rose-500/20 font-mono text-rose-400 shrink-0",                  isCompact ? "px-1 py-0 text-[8px]" : "px-1 py-0.5 text-[10px]"                )}              >                Invalid              </span>            )}          </div>          {/* Bound Sub-breakdown if present and NOT compact */}          {!isCompact && (lower !== null || upper !== null) && rangeState === "valid" && (            <div className="tooltip-detail-row flex items-center justify-between text-[10px] font-mono text-zinc-400 pl-4">              <span>{series.lowerLabel || "Lower"}: {lower !== null ? valueFormatter(lower) : "—"}</span>              <span>{series.upperLabel || "Upper"}: {upper !== null ? valueFormatter(upper) : "—"}</span>            </div>          )}        </div>        {/* Optional Centerline Readout */}        {series.valueKey && (          <div            className={cn(              "tooltip-row flex items-center justify-between border-t border-white/[0.06] gap-2",              isCompact ? "pt-1 text-[10px]" : "pt-1.5 text-[11px]"            )}          >            <span className="flex items-center gap-1 font-medium text-zinc-300 truncate">              <span                className={cn(                  "rounded-full shrink-0",                  isCompact ? "w-2 h-0.5" : "w-2.5 h-0.5"                )}                style={{ backgroundColor: valueColor }}              />              <span className="truncate">{series.valueLabel || "Center"}</span>            </span>            {valueState === "valid" && value !== null ? (              <span className="font-mono font-semibold text-white shrink-0">                {valueFormatter(value)}              </span>            ) : (              <span                className={cn(                  "rounded bg-zinc-800/60 font-mono text-zinc-500 shrink-0",                  isCompact ? "px-1 py-0 text-[8px]" : "px-1 py-0.5 text-[10px]"                )}              >                Unavailable              </span>            )}          </div>        )}      </div>    </div>  )}/* -------------------------------------------------------------------------- *//*  Main Component: RangeArea                                                 *//* -------------------------------------------------------------------------- */export function RangeArea<TData extends Record<string, unknown> = Record<string, unknown>>({  data,  xKey,  series,  height = 320,  curve = "linear",  domain,  color = "var(--chart-1)",  valueColor = "var(--chart-2)",  fillOpacity = 0.25,  selectionColor = "var(--chart-selection)",  showGrid = true,  showXAxis = true,  showYAxis = true,  showLegend = false,  lockableTooltip = true,  motion = true,  animation = "draw",  xFormatter,  valueFormatter: customValueFormatter,  title = "Range Area Chart",  description,  loading = false,  unavailable = false,  error = null,  onActiveDatumChange,  className,}: RangeAreaProps<TData>) {  const reducedMotion = useChartReducedMotion()  const motionEnabled = motion !== false && !reducedMotion && animation !== "none"  const animationDuration = typeof motion === "object" && motion?.duration ? motion.duration * 1000 : 450  const valueFormatter = React.useCallback(    (val: number) => {      if (series.valueFormatter) return series.valueFormatter(val)      if (customValueFormatter) return customValueFormatter(val)      return val.toLocaleString()    },    [series.valueFormatter, customValueFormatter]  )  const isCompact = typeof height === "number" ? height <= 260 : false  // 1. Normalize data with truthful interval validation  const { rows } = React.useMemo(    () => normalizeRangeAreaData(data, xKey, series),    [data, xKey, series]  )  // 2. Derive safe Cartesian Y-domain  const safeDomain = React.useMemo(    () => calculateRangeAreaDomain(rows, domain),    [rows, domain]  )  // 3. Inspection state: active hover index vs persistent locked index  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null)  const [lockedIndex, setLockedIndex] = React.useState<number | null>(null)  const activeIndex = lockedIndex !== null ? lockedIndex : hoverIndex  // Clear locked state if data shrinks  React.useEffect(() => {    if (lockedIndex !== null && (lockedIndex >= rows.length || rows.length === 0)) {      setLockedIndex(null)    }  }, [rows.length, lockedIndex])  // Construct active datum  const activeDatum = React.useMemo<RangeAreaActiveDatum<TData> | null>(() => {    if (activeIndex === null || activeIndex < 0 || activeIndex >= rows.length) {      return null    }    const r = rows[activeIndex]    return {      index: activeIndex,      x: r.__x,      raw: r.__raw,      lower: r.__lower,      upper: r.__upper,      value: r.__value,      rangeState: r.__rangeState,      valueState: r.__valueState,      isLocked: lockedIndex !== null && lockedIndex === activeIndex,    }  }, [activeIndex, lockedIndex, rows])  React.useEffect(() => {    onActiveDatumChange?.(activeDatum)  }, [activeDatum, onActiveDatumChange])  // Keyboard navigation & lock management  const handleKeyDown = React.useCallback(    (e: React.KeyboardEvent) => {      if (!rows.length) return      const maxIdx = rows.length - 1      const current = activeIndex ?? 0      switch (e.key) {        case "ArrowRight":        case "ArrowDown": {          e.preventDefault()          const next = Math.min(maxIdx, current + 1)          if (lockedIndex !== null) setLockedIndex(next)          else setHoverIndex(next)          break        }        case "ArrowLeft":        case "ArrowUp": {          e.preventDefault()          const prev = Math.max(0, current - 1)          if (lockedIndex !== null) setLockedIndex(prev)          else setHoverIndex(prev)          break        }        case "Home": {          e.preventDefault()          if (lockedIndex !== null) setLockedIndex(0)          else setHoverIndex(0)          break        }        case "End": {          e.preventDefault()          if (lockedIndex !== null) setLockedIndex(maxIdx)          else setHoverIndex(maxIdx)          break        }        case "Enter":        case " ": {          if (!lockableTooltip) return          e.preventDefault()          if (lockedIndex !== null) {            setLockedIndex(null)          } else {            setLockedIndex(current)          }          break        }        case "Escape": {          if (lockedIndex !== null) {            e.preventDefault()            setLockedIndex(null)          }          break        }      }    },    [rows.length, activeIndex, lockedIndex, lockableTooltip]  )  const handleChartClick = React.useCallback(    (state: any) => {      if (!lockableTooltip) return      if (state && state.activeTooltipIndex !== undefined) {        const clickedIndex = Number(state.activeTooltipIndex)        if (lockedIndex === clickedIndex) {          setLockedIndex(null)        } else {          setLockedIndex(clickedIndex)        }      } else if (lockedIndex !== null) {        setLockedIndex(null)      }    },    [lockableTooltip, lockedIndex]  )  /* ------------------------------------------------------------------------ */  /*  Early Return States                                                     */  /* ------------------------------------------------------------------------ */  if (loading) {    return (      <figure        role="region"        aria-label={title || "Range area loading state"}        className={cn("plotcn-range-area relative w-full overflow-hidden rounded-xl border border-white/10 bg-zinc-950 p-4", className)}        style={{ height, minHeight: typeof height === "number" ? height : 320 }}      >        <ChartLoadingState description="Loading range envelope..." />      </figure>    )  }  if (unavailable) {    return (      <figure        role="region"        aria-label={title || "Range area unavailable state"}        className={cn("plotcn-range-area relative w-full overflow-hidden rounded-xl border border-white/10 bg-zinc-950 p-4", className)}        style={{ height, minHeight: typeof height === "number" ? height : 320 }}      >        <ChartUnavailableState          title="Envelope Unavailable"          description={typeof unavailable === "string" ? unavailable : "Range envelope data is currently unavailable."}        />      </figure>    )  }  if (error) {    const errorDescription = error instanceof Error ? error.message : typeof error === "string" ? error : "An error occurred."    return (      <figure        role="region"        aria-label={title || "Range area error state"}        className={cn("plotcn-range-area relative w-full overflow-hidden rounded-xl border border-rose-500/20 bg-zinc-950 p-4", className)}        style={{ height, minHeight: typeof height === "number" ? height : 320 }}      >        <ChartErrorState          title="Range Area Configuration Error"          description={errorDescription}        />      </figure>    )  }  if (!data || data.length === 0 || !series) {    return (      <figure        role="region"        aria-label={title || "Range area empty state"}        className={cn("plotcn-range-area relative w-full overflow-hidden rounded-xl border border-white/10 bg-zinc-950 p-4", className)}        style={{ height, minHeight: typeof height === "number" ? height : 320 }}      >        <ChartEmptyState          title="No Range Data"          description="Supply an array of records and configure lower and upper keys."        />      </figure>    )  }  const activeRow = activeIndex !== null ? rows[activeIndex] : null  return (    <figure      role="region"      aria-label={title}      tabIndex={0}      onKeyDown={handleKeyDown}      className={cn(        "group relative flex flex-col justify-between rounded-xl border border-white/[0.08] bg-zinc-950/80 p-4 font-sans text-zinc-200 outline-none transition-all duration-150 focus-visible:ring-2 focus-visible:ring-sky-500/50",        className      )}      style={{        height,        minHeight: typeof height === "number" ? height : 320,      }}    >      {/* Screen Reader Live Narration */}      <div className="sr-only" aria-live="polite">        {description ||          `${title}. Bounded range envelope across ${rows.length} observations.${series.valueKey ? " Includes centerline." : ""}`}        {activeDatum && (          activeDatum.rangeState === "valid" && activeDatum.lower !== null && activeDatum.upper !== null            ? ` Active coordinate ${String(activeDatum.x)}. ${series.label}: ${valueFormatter(activeDatum.lower)} to ${valueFormatter(activeDatum.upper)}.${                activeDatum.value !== null ? ` ${series.valueLabel || "Center"}: ${valueFormatter(activeDatum.value)}.` : ""              }`            : activeDatum.rangeState === "invalid"            ? ` Active coordinate ${String(activeDatum.x)}. Range invalid because lower bound exceeds upper bound.`            : ` Active coordinate ${String(activeDatum.x)}. Range interval unavailable.`        )}      </div>      {/* Optional Legend */}      {showLegend && (        <div          role="group"          aria-label="Series identity legend"          className="flex flex-wrap items-center justify-center gap-3 pb-2 text-xs select-none shrink-0"        >          <span className="flex items-center gap-1.5">            <span              className="w-3 h-2 rounded-xs border border-white/20"              style={{ backgroundColor: color, opacity: fillOpacity }}            />            <span className="text-zinc-300 font-medium">{series.label}</span>          </span>          {series.valueKey && (            <span className="flex items-center gap-1.5">              <span className="w-3 h-0.5 rounded-full" style={{ backgroundColor: valueColor }} />              <span className="text-zinc-300 font-medium">{series.valueLabel || "Center"}</span>            </span>          )}        </div>      )}      {/* SVG Canvas Container */}      <ChartContainer className="relative w-full flex-1 min-w-0 min-h-0 max-w-full overflow-hidden">        <ResponsiveContainer          width="100%"          height="100%"          minWidth={0}          minHeight={0}          initialDimension={{ width: 320, height: typeof height === "number" ? height : 320 }}        >          <ComposedChart            data={rows}            onClick={handleChartClick}            onMouseMove={(state: any) => {              if (lockedIndex === null && state && state.activeTooltipIndex !== undefined) {                setHoverIndex(Number(state.activeTooltipIndex))              }            }}            onMouseLeave={() => {              if (lockedIndex === null) {                setHoverIndex(null)              }            }}            margin={{ top: 12, right: 16, left: showYAxis ? -10 : 4, bottom: 4 }}          >            {showGrid && (              <CartesianGrid                strokeDasharray="3 3"                vertical={false}                stroke="rgba(255,255,255,0.06)"              />            )}            {showXAxis && (              <XAxis                dataKey="__x"                tickLine={false}                axisLine={{ stroke: "rgba(255,255,255,0.12)" }}                tick={{ fill: "#a1a1aa", fontSize: 11, fontFamily: "monospace" }}                tickFormatter={xFormatter}                dy={6}              />            )}            {showYAxis && (              <YAxis                domain={safeDomain as any}                tickFormatter={valueFormatter}                tickLine={false}                axisLine={{ stroke: "rgba(255,255,255,0.12)" }}                tick={{ fill: "#a1a1aa", fontSize: 11, fontFamily: "monospace" }}                width={48}              />            )}            {/* Neutral Crosshair Line */}            {activeRow && (              <ReferenceLine                x={activeRow.__x}                stroke={selectionColor}                strokeWidth={1.5}                strokeDasharray={lockedIndex !== null ? "none" : "3 3"}              />            )}            {/* Tooltip */}            <Tooltip              isAnimationActive={false}              cursor={false}              content={                <RangeAreaTooltipContent                  activeDatum={activeDatum}                  series={series}                  color={color}                  valueColor={valueColor}                  valueFormatter={valueFormatter}                  onUnlock={() => setLockedIndex(null)}                  isCompact={isCompact}                />              }            />            {/* Bounded Range Envelope Area: fills between lower and upper bounds */}            <Area              type={curve}              dataKey="__range"              name={series.label}              fill={color}              fillOpacity={fillOpacity}              stroke={color}              strokeWidth={1.2}              strokeOpacity={0.65}              isAnimationActive={motionEnabled}              animationDuration={animationDuration}              animationEasing="ease-out"              activeDot={false}              dot={false}              connectNulls={false}            />            {/* Optional Central Signal Line */}            {series.valueKey && (              <Line                type={curve}                dataKey="__value"                name={series.valueLabel || "Center"}                stroke={valueColor}                strokeWidth={2}                dot={false}                activeDot={{                  r: 4,                  fill: valueColor,                  stroke: "var(--chart-background, #09090b)",                  strokeWidth: 2,                }}                isAnimationActive={motionEnabled}                animationDuration={animationDuration}                animationEasing="ease-out"                connectNulls={false}              />            )}          </ComposedChart>        </ResponsiveContainer>      </ChartContainer>      {/* Structured Data Alternative for Screen Readers */}      <div className="sr-only">        <table>          <caption>{title || `${series.label} observation data table`}</caption>          <thead>            <tr>              <th scope="col">Coordinate</th>              <th scope="col">{series.lowerLabel || "Lower"}</th>              <th scope="col">{series.upperLabel || "Upper"}</th>              {series.valueKey && <th scope="col">{series.valueLabel || "Center"}</th>}              <th scope="col">Interval State</th>            </tr>          </thead>          <tbody>            {rows.map((r, i) => (              <tr key={i}>                <td>{String(r.__x)}</td>                <td>{r.__lower !== null ? valueFormatter(r.__lower) : "—"}</td>                <td>{r.__upper !== null ? valueFormatter(r.__upper) : "—"}</td>                {series.valueKey && <td>{r.__value !== null ? valueFormatter(r.__value) : "—"}</td>}                <td>{r.__rangeState}</td>              </tr>            ))}          </tbody>        </table>      </div>    </figure>  )}