015 / RECHARTS / AREA

Comparison Area

Recharts

Two-series area visualization for comparing a primary magnitude against a reference magnitude across an ordered domain, using restrained overlap, deterministic semantic identity, and shared Y-scale.

SPEC
#015
ENGINE
Recharts
FAMILY
Area
RENDERER
svg
STATUS
preview

Installation

PLOTCN/REGISTRY/AREA-COMPARISON/SOURCE
pnpm dlx shadcn@latest add @plotcn/area-comparison

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

Comparison Area is the comparative-specialized chart of the Plotcn Area family. It visualizes a dominant primary magnitude against a reference magnitude across the same ordered domain, filling both toward a shared baseline while using restrained overlap and deterministic visual hierarchy.

The primary analytical question answered by Comparison Area is:

"How does the primary series compare with the reference across the same domain?"

The secondary analytical question is:

"At this observation, what is the factual difference between them?"

Unlike multi-series stacked charts that accumulate values into an aggregate sum, Comparison Area displays independent magnitudes measured in the exact same unit on a shared Y-axis. The overlap between the two areas communicates comparison—it is not an additive contribution nor a separate third metric.

TSX
import { ComparisonArea } from "@/components/charts/recharts/area-comparison"const revenueData = [  { month: "Jan", current: 125, previous: 110 },  { month: "Feb", current: 142, previous: 120 },  { month: "Mar", current: 138, previous: 135 },  { month: "Apr", current: 165, previous: 140 },  { month: "May", current: 158, previous: 162 },  { month: "Jun", current: 184, previous: 155 },]export function MonthlyRevenueComparison() {  return (    <ComparisonArea      data={revenueData}      xKey="month"      series={{        primary: { key: "current", label: "2026 (Current)" },        reference: { key: "previous", label: "2025 (Previous)" },      }}      primaryColor="var(--chart-1)"      referenceColor="var(--chart-2)"      valueFormatter={(v) => `$${v.toLocaleString()}k`}      showGrid      showLegend    />  )}

Area-Family Positioning

The Plotcn Area family provides five distinct analytical instruments:

Consideration Prism Area (011) Stack Flow Area (012) Percent Stream Area (013) Range Area (014) Comparison Area (015)
ConsiderationPrism Area (011)Stack Flow Area (012)Percent Stream Area (013)Range Area (014)Comparison Area (015)
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 contains the observations?""How does primary compare against reference?"
Fill GeometryFills between metric and baselineFills additive layers from bottom to topFills 100% normalized proportional sharesFills between lower and upper boundsFills both toward the same baseline (overlapping)
Baseline RoleConfigurable baseline (y=0y = 0 or min)Fixed baseline (y=0y = 0)Fixed baseline (0%0\%)No baseline fill; lower bound is floorSingle shared baseline for both roles
Series RolesSingle seriesNN additive seriesNN proportional seriesBounded envelope + optional centerlineExactly two roles: Primary & Reference
Overlap TreatmentNot applicableStacks without overlapStacks to 100%Single bounded bandRestrained translucent overlap

Comparison Area vs Twinline Compare (003)

  • Twinline Compare: Focuses purely on linear trajectories, precise line crossings, and high-density multi-point inspection where shaded fills would create excessive visual clutter.
  • Comparison Area: Focuses on quantitative magnitude relative to a shared baseline. Shaded fills convey volume and cumulative presence, making cumulative gaps and overall scale differences immediately intuitive.

Comparison Model

COMPARISON MODELPrimary vs Reference Overlap

Restrained Overlap Across Shared Baseline & Single Y-Scale

Primary (Solid 2px)
Reference (Dashed 1.5px)
Overlap
$160k$120k$80k$40ky = 0 (Baseline)JanFebMarAprMayJunJulrestrained overlapApr 2026 (Locked)Current year$142,000Previous year$126,000Difference (Δ)+$16,000
Truthful Comparison: Both areas measure individual magnitudes from baseline (y = 0); overlap communicates co-presence, not composition.
Non-color identity: Primary (solid 2px) vs Reference (dashed 1.5px)

Primary and Reference:

  1. Share the exact same horizontal X domain.
  2. Share the exact same quantitative measurement unit.
  3. Share the exact same Y scale and zero-reference baseline.
  4. Overlap with controlled alpha rather than stacking vertically.

Comparison vs Stacking

CRITICAL DISTINCTIONComparison vs Additive Composition

Why Comparison Areas Must Overlap, Never Stack

✓ Comparison Area (Correct)Shared Baseline (y = 0)

Both series fill upward from zero. Visual height truthfully reflects independent period magnitudes ($140k vs $120k).

y=0$140k$120koverlap: $120k
Scale Peak: $140k (Primary)Delta: +$20k
✕ Stacked Area (Misleading)Additive Stacking Anti-Pattern

Primary sits on top of reference. Visual peak reaches $260k, creating the false illusion of a combined $260k revenue total.

y=0Previous ($120k)Current ($140k stacked)$260k
False Total: $260k (Sum)Misrepresents comparison
Analytical Rule: When comparing two periods or cohorts, use Comparison Area overlap—never additive stacking.
Stacking alters the analytical question from comparison to composition

A frequent charting anti-pattern is using a stacked area chart when comparing two time periods:

In a stacked area chart, the visual top of the stack would reach $260k when comparing $140k against $120k, misleading viewers into believing that revenue totaled $260k in that period.

In Comparison Area, both series start from the baseline (y=0y = 0). At that coordinate, the chart clearly shows two heights: 140k140k and 120k120k, with an overlapping shaded region of 120k120k and an upper primary extension of 20k20k.

Overlap Treatment

Comparison Area deliberately enforces strict structural hierarchy so both roles remain clearly identifiable even when printed in grayscale or rendered with identical colors:

TEXT
Primary Role:→ Rendered SECOND (on top)→ Stronger opacity (default: 0.28)→ Solid 2px boundary stroke→ Solid rectangle indicator in legendReference Role:→ Rendered FIRST (underneath)→ Quieter opacity (default: ~0.14)→ Dashed boundary stroke (strokeDasharray="4 3")→ Dashed indicator in legend

Difference Semantics

When both primary and reference observations exist at an inspected coordinate, the inspection card computes the factual delta:

Δ=PrimaryReference\Delta = \text{Primary} - \text{Reference}
  • Factual Arithmetic: The difference represents pure arithmetic. It is formatted with a leading sign (+16,000 or −4,200).
  • Neutral Styling: Plotcn renders deltas in neutral typography. A positive delta is not inherently "good" (green) and a negative delta is not inherently "bad" (red)—in metrics like server latency, defect rates, or infrastructure costs, higher values represent regressions.
  • Truthful Missing Handling: If either primary or reference is missing at coordinate xx, the delta row reports "Unavailable". Zero is never substituted.

Data Contract

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

TypeScript
export interface RevenueComparisonPoint {  month: string             // Chronological or ordinal domain key  current: number | null    // Primary series magnitude  previous: number | null   // Reference series magnitude}

Truthful Missing Values

Condition at Coordinate $x$ Primary Area Reference Area Tooltip Primary Tooltip Reference Delta Readout
Condition at Coordinate xxPrimary AreaReference AreaTooltip PrimaryTooltip ReferenceDelta Readout
Both ValidRenderedRenderedFormatted valueFormatted valueFormatted Δ\Delta
Primary Missing (nullnull)Visual gapRendered"Unavailable"Formatted value"Unavailable"
Reference Missing (nullnull)RenderedVisual gapFormatted value"Unavailable""Unavailable"
Both Missing (nullnull)Visual gapVisual gap"Unavailable""Unavailable""Unavailable"
Primary or Reference = 0Rendered at y=0y=0Rendered at y=0y=0$0$0Formatted Δ\Delta

Series Contract

Comparison Area requires exactly two semantic roles configured via the series prop:

Role Property Type Required Description
RolePropertyTypeRequiredDescription
PrimarykeyNumericKeyOf<TData>YesKey in observation records for primary magnitude
PrimarylabelstringYesDescriptive label for primary series (e.g. "Current year")
PrimarycolorstringOptionalOptional series-level color override
ReferencekeyNumericKeyOf<TData>YesKey in observation records for reference magnitude
ReferencelabelstringYesDescriptive label for reference series (e.g. "Previous year")
ReferencecolorstringOptionalOptional series-level color override

Color Customization

Comparison Area accepts semantic CSS theme tokens or explicit custom color overrides:

TSX
<ComparisonArea  data={data}  xKey="month"  series={{    primary: { key: "current", label: "Current year" },    reference: { key: "previous", label: "Previous year" },  }}  primaryColor="var(--chart-1)"  referenceColor="var(--chart-2)"  selectionColor="var(--chart-selection)"  fillOpacity={0.28}/>
  • Theme Synchronization: Changing primaryColor or referenceColor instantly updates fills, boundaries, active inspection markers, and legend indicators without remounting or resetting inspection state.
  • Same-Color Distinction: If primaryColor === referenceColor, the primary solid 2px stroke and 0.28 opacity remain readily distinguishable from the reference dashed 1.5px stroke and 0.14 opacity.

Props Reference

Property Type Default Description
PropertyTypeDefaultDescription
datareadonly TData[][]Readonly array of observation records. Caller data is never mutated.
xKeykeyof TData & stringrequiredProperty name representing horizontal domain coordinates.
seriesComparisonAreaSeries<TData>requiredSemantic configuration defining primary and reference keys and labels.
heightnumber | string320Container height in pixels or CSS units.
curve"monotone" | "linear" | "step""monotone"Shared interpolation curve applied identically to both series.
domain[number, number] | "auto""auto"Shared vertical domain override covering both roles.
primaryColorstringvar(--chart-1)Color for primary area fill, boundary stroke, and legend indicator.
referenceColorstringvar(--chart-2)Color for reference area fill, boundary stroke, and legend indicator.
fillOpacitynumber0.28Primary area fill opacity. Reference opacity derives quieter (~0.14).
showDeltabooleantrueWhether to render factual difference arithmetic in tooltips.
deltaType"absolute" | "percentage" | "both""both"Formatting style for comparison delta.
showGridbooleantrueWhether to render horizontal Cartesian reference rules.
showXAxisbooleantrueWhether to render horizontal domain tick labels.
showYAxisbooleantrueWhether to render vertical metric scale ticks.
showLegendbooleantrueWhether to render series identity legend with structural samples.
interactiveLegendbooleantrueEnables toggling role visibility by clicking legend items.
lockableTooltipbooleantrueEnables persistent tooltip pinning on click, tap, or Enter/Space.
motionboolean | { duration?: number }trueEntry reveal animation. Automatically bypassed under reduced motion.
valueFormatter(value: number) => stringn.toLocaleString()Formatter for metric values and comparison delta.
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):
<ComparisonArea
  data={data}
  xKey="date"
  series={{
    primary: { key: "current", label: "Current year" },
    reference: { key: "previous", label: "Previous year" },
  }}
/>
Interactive Prop Preview Lab
curve"linear" | "monotone" | "step"

Shared interpolation algorithm applied identically to both areas.

Select value to preview live:
Active: curve="monotone"Default: "monotone"
primaryColorstring

Color for primary area fill, solid boundary stroke, and legend sample.

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

Color for reference area fill, dashed boundary stroke, and legend sample.

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

Primary area fill opacity (0.05 to 1.0). Reference opacity derives quieter (~0.14).

Select value to preview live:
Active: fillOpacity={0.28}Default: 0.28
showDeltaboolean

Whether to display factual comparison arithmetic (primary − reference) in the tooltip.

Select value to preview live:
Active: showDelta={true}Default: true
deltaType"absolute" | "percentage" | "both"

Format for comparison delta readout in the inspection card.

Select value to preview live:
Active: deltaType="both"Default: "both"
showGridboolean

Whether to render horizontal Cartesian reference rules.

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

Whether to render the series identity legend with non-color structural samples.

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

Enables clicking legend items to toggle role visibility.

Select value to preview live:
Active: interactiveLegend={true}Default: true
lockableTooltipboolean

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

Select value to preview live:
Active: lockableTooltip={true}Default: true
All Properties (23)
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.

ComparisonAreaSeries<TData>Yes

Explicit semantic configuration defining primary and reference roles.

number | string320No

Container height in pixels or standard CSS dimension strings.

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

Shared interpolation algorithm applied identically to both areas.

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

Color for primary area fill, solid boundary stroke, and legend sample.

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

Color for reference area fill, dashed boundary stroke, and legend sample.

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

Color for active locked inspection crosshair.

number0.28No

Primary area fill opacity (0.05 to 1.0). Reference opacity derives quieter (~0.14).

booleantrueNo

Whether to display factual comparison arithmetic (primary − reference) in the tooltip.

"absolute" | "percentage" | "both""both"No

Format for comparison delta readout in the inspection card.

booleantrueNo

Whether to render horizontal Cartesian reference rules.

booleantrueNo

Whether to render horizontal category scale ticks.

booleantrueNo

Whether to render vertical metric scale ticks.

booleantrueNo

Whether to render the series identity legend with non-color structural samples.

booleantrueNo

Enables clicking legend items to toggle role visibility.

booleantrueNo

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

"gap" | "connect""gap"No

Handling of missing / null values.

(value: number) => stringv => v.toLocaleString()No

Custom formatter for metric values and delta readouts.

(value: any) => stringNo

Custom formatter for horizontal domain tick labels.

boolean | { duration?: number }trueNo

Controls entry reveal animations. Automatically bypassed under reduced motion.

string"Comparison Area Chart"No

Accessible heading announced to screen readers.

stringNo

Extended descriptive summary for assistive technologies.

04 / Cookbook & States

Component Variants & Edge States

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

Monthly Revenue Comparison

Monthly recurring revenue comparing 2026 (current year) against 2025 (previous year) across identical months.

<ComparisonArea
  data={revenueData}
  xKey="month"
  series={{
    primary: { key: "current", label: "2026 (Current)" },
    reference: { key: "previous", label: "2025 (Previous)" },
  }}
  primaryColor="var(--chart-1)"
  referenceColor="var(--chart-2)"
  valueFormatter={(v) => `$${v.toLocaleString()}k`}
  showGrid
  showLegend
/>

Same-Color Structural Distinction

Demonstrates non-color differentiation: even when both series use identical colors, primary (solid border, stronger fill) and reference (dashed border, quieter fill) remain immediately readable.

<ComparisonArea
  data={revenueData}
  xKey="month"
  series={{
    primary: { key: "current", label: "Current" },
    reference: { key: "previous", label: "Reference" },
  }}
  primaryColor="var(--foreground)"
  referenceColor="var(--foreground)"
  fillOpacity={0.3}
  showGrid
  showLegend
/>

Crossing Trajectories

Primary and reference series cross multiple times over the domain, validating that restrained opacity preserves legibility throughout intersections without flashy blend modes.

<ComparisonArea
  data={crossingData}
  xKey="month"
  series={{
    primary: { key: "actual", label: "Actual" },
    reference: { key: "target", label: "Target" },
  }}
  showGrid
  showLegend
/>

Independent Missing Values

Truthful gap rendering when observations are missing in either series. A missing reference leaves the primary intact and marks difference unavailable.

<ComparisonArea
  data={missingData}
  xKey="month"
  series={{
    primary: { key: "current", label: "Current" },
    reference: { key: "previous", label: "Previous" },
  }}
  missingValuePolicy="gap"
  showGrid
  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

Comparison Area preserves both primary and reference layers, non-color structural samples, and active inspection slice across all device widths down to 320px.

Desktop
>= 1024px

Spacious overlap geometry, full domain ticks, horizontal legend, and comprehensive inspection tooltip with delta readout.

Tablet
640px - 1023px

Adaptive domain tick thinning, preserved primary/reference hierarchy, and wrapped legend controls.

Mobile
< 640px

Compact gutters, full dual-layer area geometry without dropping either role, and compact stacked tooltip preventing horizontal 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

Primary solid stroke (2px) versus Reference dashed stroke (1.5px, 4 3), opacity attenuation, and structured data table ensure complete non-color accessibility.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “Announces domain coordinate, primary value, and reference value. Factual arithmetic delta is narrated without good/bad value judgments.

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. Deterministic Semantic Roles: Primary and Reference represent fixed semantic roles. Neither role inherits the other's identity when hidden.
  2. One Shared Y Scale: Primary and Reference are always measured against the exact same scale and baseline. Dual-axis scaling is prohibited.
  3. No Stacking Distortion: Primary and Reference overlap independently. Values are never added together to fabricate a combined sum.
  4. No Fabricated Data: Missing values (nullnull) produce explicit breaks in the area geometry; zero is never substituted.
  5. Finite Coordinates Only: Non-finite values (NaN, Infinity) are sanitized before rendering SVG paths.
  6. Neutral Comparison: Difference arithmetic is presented factually without forced positive/negative value judgments.
  7. Accessibility Disclosure: Structured HTML data table disclosure provides full non-visual access to all primary, reference, and difference values.
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
ComparisonArea(Root figure element with keyboard navigation and ARIA accessibility shell)
├──ResponsiveContainer[Responsive container wrapper]

Handles container dimension measurement and SVG viewBox sizing

├──AreaChart[Recharts Cartesian SVG coordinator]

Coordinates scales, Cartesian grid, reference area, primary area, and shared tooltip

└──ComparisonAreaLegend[Series identity and filter controls]

Encodes structural differences (solid vs dashed) and supports visibility toggling

Involved Source Files & Registry Assets
registry/recharts/area-comparison.tsx
Complete Comparison Area component with primary/reference roles, restrained overlap, and non-color identity
registry/recharts/area-comparison.tsx
"use client"import * as React from "react"import {  ResponsiveContainer,  AreaChart,  Area,  XAxis,  YAxis,  Tooltip,  CartesianGrid,  Legend,  type TooltipProps,} from "recharts"import { useChartReducedMotion } from "../shared/use-chart-reduced-motion"import { ChartContainer } from "../shared/chart-container"import {  ChartLoadingState,  ChartEmptyState,  ChartErrorState,  ChartUnavailableState,} from "../shared/chart-state"import { cn } from "@/lib/utils"import { HugeiconsIcon } from "@hugeicons/react"import { LockKeyIcon } from "@hugeicons/core-free-icons"/* -------------------------------------------------------------------------- *//*  Type Definitions                                                          *//* -------------------------------------------------------------------------- */export type CurveType = "linear" | "monotone" | "step"export type DeltaType = "absolute" | "percentage" | "both"export type NumericKeyOf<T> = {  [K in keyof T]: T[K] extends number | null | undefined ? K : never}[keyof T] & stringexport interface ComparisonAreaSeriesItem<TData = Record<string, unknown>> {  /** Property key on datum representing this role's numeric metric */  key: keyof TData & string  /** Human-readable display label */  label: string  /** Optional role-specific color override */  color?: string  /** Optional custom value formatter */  valueFormatter?: (value: number) => string}export interface ComparisonAreaSeries<TData = Record<string, unknown>> {  /** The primary (current / dominant) series */  primary: ComparisonAreaSeriesItem<TData>  /** The reference (baseline / previous period) series */  reference: ComparisonAreaSeriesItem<TData>}export interface ComparisonAreaProps<TData extends Record<string, unknown> = Record<string, unknown>> {  /**   * The array of observation records to visualize.   * Readonly array; caller data is never mutated.   */  data: readonly TData[]  /**   * Property name for horizontal domain coordinates (time, date, category).   */  xKey: keyof TData & string  /**   * Explicit primary and reference series configuration.   */  series: ComparisonAreaSeries<TData>  /**   * Container height in pixels or CSS dimension string.   * Default: 320   */  height?: number | string  /**   * Curve interpolation algorithm.   * Default: "monotone"   */  curve?: CurveType  /**   * Shared vertical domain [min, max] or "auto".   * Automatically computed across both roles to guarantee honest comparison.   */  domain?: [number, number] | "auto"  /**   * Primary series color.   * Default: "var(--chart-1)"   */  primaryColor?: string  /**   * Reference series color.   * Default: "var(--chart-2)"   */  referenceColor?: string  /**   * Selection / locked indicator color.   * Default: "var(--chart-selection)"   */  selectionColor?: string  /**   * Primary area fill opacity (0.05 to 1.0).   * Default: 0.28   */  fillOpacity?: number  /**   * Explicit primary fill opacity override.   */  primaryFillOpacity?: number  /**   * Explicit reference fill opacity override.   * Defaults to a restrained attenuation of primary opacity (~0.14).   */  referenceFillOpacity?: number  /**   * Handling of missing / null values.   * "gap" produces a truthful visual break; "connect" interpolates across missing coordinates.   * Default: "gap"   */  missingValuePolicy?: "gap" | "connect"  /**   * Whether to display comparison delta (primary − reference) in the tooltip.   * Default: true   */  showDelta?: boolean  /**   * Format for delta readout: "absolute", "percentage", or "both".   * Default: "both"   */  deltaType?: DeltaType  /**   * Whether to invert delta semantics (e.g. for latency or error counts where lower is better).   * Default: false   */  invertDelta?: boolean  /**   * Custom formatter for metric values.   */  valueFormatter?: (value: number) => string  /**   * Custom formatter for horizontal domain ticks.   */  xFormatter?: (value: any) => string  /**   * Whether to display Cartesian background grid lines.   * Default: true   */  showGrid?: boolean  /**   * Whether to display horizontal X axis ticks.   * Default: true   */  showXAxis?: boolean  /**   * Whether to display vertical Y axis ticks.   * Default: true   */  showYAxis?: boolean  /**   * Whether to render the series legend.   * Default: true   */  showLegend?: boolean  /**   * Whether legend items can be clicked to toggle role visibility.   * Default: true   */  interactiveLegend?: boolean  /**   * Enables persistent tooltip pinning on click, tap, or Enter/Space.   * Default: true   */  lockableTooltip?: boolean  /**   * Controls entry transitions. Automatically bypassed under reduced motion.   * Default: true   */  motion?: boolean | { duration?: number }  /**   * Accessible name announced by screen readers for the `<figure>` container.   * Default: "Comparison Area Chart"   */  title?: string  /**   * Extended description for assistive technologies.   */  description?: string  /**   * Displays loading placeholder skeleton while preserving dimensions.   */  loading?: boolean  /**   * Error state indicator or Error instance.   */  error?: Error | string | null  /**   * Unavailable notice (e.g. tier restrictions or retention limits).   */  unavailable?: boolean | string | null  /**   * Callback invoked when user clicks error retry.   */  onRetry?: () => void  /**   * Callback fired when active hovered/locked observation changes.   */  onActiveDatumChange?: (datum: TData | null) => void  /**   * Additional CSS classes for root figure container.   */  className?: string}/* -------------------------------------------------------------------------- *//*  Pure Math & Domain Safety Helpers                                         *//* -------------------------------------------------------------------------- */function isFiniteNumber(val: unknown): val is number {  return typeof val === "number" && Number.isFinite(val)}/** * Calculates a single honest shared Y-domain covering both Primary and Reference. * Guarantees zero-division safety and truthful scaling. */export function calculateSharedDomain(  data: readonly Record<string, unknown>[],  primaryKey: string,  referenceKey: string,  explicitDomain?: [number, number] | "auto",  visibleSeries: Set<"primary" | "reference"> = new Set(["primary", "reference"])): [number, number] {  if (explicitDomain && explicitDomain !== "auto") {    return explicitDomain  }  const values: number[] = []  for (const row of data) {    if (visibleSeries.has("primary")) {      const p = row[primaryKey]      if (isFiniteNumber(p)) values.push(p)    }    if (visibleSeries.has("reference")) {      const r = row[referenceKey]      if (isFiniteNumber(r)) values.push(r)    }  }  if (values.length === 0) {    return [0, 100]  }  const min = Math.min(...values)  const max = Math.max(...values)  // Single-value scale protection  if (min === max) {    if (min === 0) return [-1, 1]    if (min > 0) return [0, Math.ceil(min * 1.25)]    return [Math.floor(min * 1.25), 0]  }  // Cross-zero or positive-only padding  const span = max - min  const pad = span * 0.08  const safeMin = min >= 0 ? 0 : Math.floor(min - pad)  const safeMax = Math.ceil(max + pad)  return [safeMin, safeMax]}/** * Normalizes dataset without mutating original caller objects. * Missing/non-finite observations are converted to null for truthful gap rendering. */export function normalizeComparisonData<TData extends Record<string, unknown>>(  data: readonly TData[],  xKey: string,  primaryKey: string,  referenceKey: string): Record<string, unknown>[] {  return data.map((item, idx) => {    const rawPrimary = item[primaryKey]    const rawReference = item[referenceKey]    const xVal = item[xKey] ?? `Point ${idx + 1}`    return {      ...item,      [xKey]: xVal,      [primaryKey]: isFiniteNumber(rawPrimary) ? rawPrimary : null,      [referenceKey]: isFiniteNumber(rawReference) ? rawReference : null,    }  })}/* -------------------------------------------------------------------------- *//*  Synchronized Comparison Tooltip Component                                 *//* -------------------------------------------------------------------------- */interface ComparisonAreaTooltipContentProps {  active?: boolean  payload?: readonly { dataKey?: string | number; value?: any; [key: string]: any }[]  label?: React.ReactNode  primaryKey: string  referenceKey: string  primaryLabel: string  referenceLabel: string  primaryColor: string  referenceColor: string  valueFormatter: (value: number) => string  showDelta?: boolean  deltaType?: DeltaType  invertDelta?: boolean  isCompact?: boolean  isLocked?: boolean  onUnlock?: () => void}function ComparisonAreaTooltipContent({  active,  payload,  label,  primaryKey,  referenceKey,  primaryLabel,  referenceLabel,  primaryColor,  referenceColor,  valueFormatter,  showDelta = true,  deltaType = "both",  invertDelta = false,  isCompact = false,  isLocked = false,  onUnlock,}: ComparisonAreaTooltipContentProps) {  if (!active || !payload || payload.length === 0) {    return null  }  const primaryItem = payload.find((p) => p.dataKey === primaryKey)  const referenceItem = payload.find((p) => p.dataKey === referenceKey)  const primaryVal = primaryItem?.value  const referenceVal = referenceItem?.value  const hasPrimary = isFiniteNumber(primaryVal)  const hasReference = isFiniteNumber(referenceVal)  // Delta calculation: primary − reference  let absoluteDelta: number | null = null  let percentageDelta: number | null = null  if (hasPrimary && hasReference) {    absoluteDelta = primaryVal - referenceVal    if (referenceVal !== 0) {      percentageDelta = ((primaryVal - referenceVal) / Math.abs(referenceVal)) * 100    }  }  const formatDeltaString = () => {    if (absoluteDelta === null) return "—"    const sign = absoluteDelta > 0 ? "+" : absoluteDelta < 0 ? "−" : ""    const absVal = Math.abs(absoluteDelta)    if (deltaType === "absolute") {      return `${sign}${valueFormatter(absVal)}`    }    if (deltaType === "percentage") {      if (percentageDelta === null) return `${sign}${valueFormatter(absVal)}`      const pctSign = percentageDelta > 0 ? "+" : percentageDelta < 0 ? "−" : ""      return `${pctSign}${Math.abs(percentageDelta).toFixed(1)}%`    }    // "both"    if (percentageDelta !== null) {      const pctSign = percentageDelta > 0 ? "+" : percentageDelta < 0 ? "−" : ""      return `${sign}${valueFormatter(absVal)} (${pctSign}${Math.abs(percentageDelta).toFixed(1)}%)`    }    return `${sign}${valueFormatter(absVal)}`  }  return (    <div      className={cn(        "plotcn-interactive-tooltip z-50 rounded-lg border border-[var(--chart-tooltip-border)] bg-[var(--chart-tooltip-background)] shadow-lg backdrop-blur-md select-none",        isCompact          ? "min-w-[120px] max-w-[190px] p-2 text-[10px]"          : "min-w-[190px] max-w-[270px] p-2.5 text-xs"      )}    >      {/* Header with coordinate & optional pin badge */}      <div className={cn("flex items-center justify-between gap-2 border-b border-white/[0.08] pb-1.5", isCompact ? "mb-1.5" : "mb-2")}>        <span className={cn("font-mono font-medium text-[var(--chart-tooltip-muted)] truncate", isCompact ? "text-[10px]" : "text-[11px]")}>          {label}        </span>        {isLocked && (          <button            type="button"            onClick={onUnlock}            aria-label="Unlock tooltip"            className={cn(              "flex items-center gap-1 rounded font-mono font-semibold transition-colors",              "bg-primary/20 text-primary hover:bg-primary/30",              isCompact ? "px-1 py-0.2 text-[8px]" : "px-1.5 py-0.5 text-[9px]"            )}          >            <HugeiconsIcon icon={LockKeyIcon} size={isCompact ? 9 : 11} />            <span>Locked</span>          </button>        )}      </div>      <div className={isCompact ? "space-y-1" : "space-y-1.5"}>        {/* Primary Series Row (Solid identity) */}        <div className="flex items-center justify-between gap-3">          <div className="flex items-center gap-1.5 min-w-0">            <span              className={cn("rounded-xs border border-white/20 shrink-0", isCompact ? "size-2" : "size-2.5")}              style={{ backgroundColor: primaryColor }}            />            <span className="font-medium text-[var(--chart-tooltip-foreground)] truncate">              {primaryLabel}            </span>          </div>          <span className="font-mono font-semibold text-[var(--chart-tooltip-foreground)] shrink-0">            {hasPrimary ? valueFormatter(primaryVal) : "—"}          </span>        </div>        {/* Reference Series Row (Quieter dashed identity) */}        <div className="flex items-center justify-between gap-3">          <div className="flex items-center gap-1.5 min-w-0">            <span              className={cn("rounded-xs border border-dashed border-white/40 shrink-0 opacity-80", isCompact ? "size-2" : "size-2.5")}              style={{ backgroundColor: referenceColor }}            />            <span className="font-medium text-[var(--chart-tooltip-foreground)] truncate">              {referenceLabel}            </span>          </div>          <span className="font-mono font-semibold text-[var(--chart-tooltip-foreground)] shrink-0">            {hasReference ? valueFormatter(referenceVal) : "—"}          </span>        </div>        {/* Delta Row (Neutral factual arithmetic) */}        {showDelta && (          <div className={cn("mt-2 flex items-center justify-between border-t border-white/[0.08] pt-1.5", isCompact ? "text-[9px]" : "text-[11px]")}>            <span className="text-[var(--chart-tooltip-muted)] font-medium">Difference</span>            <span className="font-mono font-medium text-[var(--chart-tooltip-foreground)]">              {formatDeltaString()}            </span>          </div>        )}      </div>    </div>  )}/* -------------------------------------------------------------------------- *//*  Custom Legend Component with Non-Color Structural Samples                 *//* -------------------------------------------------------------------------- */interface ComparisonAreaLegendProps {  primaryLabel: string  referenceLabel: string  primaryColor: string  referenceColor: string  visibleSeries: Set<"primary" | "reference">  onToggle: (role: "primary" | "reference") => void  interactive?: boolean  isCompact?: boolean}function ComparisonAreaLegend({  primaryLabel,  referenceLabel,  primaryColor,  referenceColor,  visibleSeries,  onToggle,  interactive = true,  isCompact = false,}: ComparisonAreaLegendProps) {  const isPrimaryVisible = visibleSeries.has("primary")  const isReferenceVisible = visibleSeries.has("reference")  return (    <div className={cn("flex flex-wrap items-center justify-center gap-4 pt-3 select-none", isCompact ? "text-[10px]" : "text-xs")}>      {/* Primary Legend Item (Solid boundary & fill) */}      <button        type="button"        disabled={!interactive}        onClick={() => onToggle("primary")}        aria-pressed={isPrimaryVisible}        className={cn(          "flex items-center gap-1.5 transition-opacity focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--chart-focus)] rounded px-1.5 py-0.5",          !isPrimaryVisible && "opacity-40 line-through text-muted-foreground",          interactive ? "cursor-pointer hover:opacity-80" : "cursor-default"        )}      >        <span          className="relative inline-flex items-center justify-center size-3 rounded-xs border border-white/20 shrink-0"          style={{ backgroundColor: primaryColor }}        >          <span className="w-2 h-0.5 bg-white/70 rounded-full" />        </span>        <span className="font-medium text-foreground">{primaryLabel}</span>      </button>      {/* Reference Legend Item (Dashed boundary & quieter fill) */}      <button        type="button"        disabled={!interactive}        onClick={() => onToggle("reference")}        aria-pressed={isReferenceVisible}        className={cn(          "flex items-center gap-1.5 transition-opacity focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--chart-focus)] rounded px-1.5 py-0.5",          !isReferenceVisible && "opacity-40 line-through text-muted-foreground",          interactive ? "cursor-pointer hover:opacity-80" : "cursor-default"        )}      >        <span          className="relative inline-flex items-center justify-center size-3 rounded-xs border border-dashed border-white/50 shrink-0 opacity-80"          style={{ backgroundColor: referenceColor }}        >          <span className="w-2 h-0.5 border-t border-dashed border-white/90" />        </span>        <span className="font-medium text-foreground">{referenceLabel}</span>      </button>    </div>  )}/* -------------------------------------------------------------------------- *//*  Main Component: ComparisonArea                                            *//* -------------------------------------------------------------------------- */export function ComparisonArea<TData extends Record<string, unknown> = Record<string, unknown>>({  data = [],  xKey,  series,  height = 320,  curve = "monotone",  domain,  primaryColor = "var(--chart-1)",  referenceColor = "var(--chart-2)",  selectionColor = "var(--chart-selection)",  fillOpacity = 0.28,  primaryFillOpacity,  referenceFillOpacity,  missingValuePolicy = "gap",  showDelta = true,  deltaType = "both",  invertDelta = false,  valueFormatter = (v: number) => v.toLocaleString(),  xFormatter,  showGrid = true,  showXAxis = true,  showYAxis = true,  showLegend = true,  interactiveLegend = true,  lockableTooltip = true,  motion = true,  title = "Comparison Area Chart",  description,  loading = false,  error = null,  unavailable = false,  onRetry,  onActiveDatumChange,  className,}: ComparisonAreaProps<TData>) {  const isReducedMotion = useChartReducedMotion()  // Diagnostic warning for duplicate keys  React.useEffect(() => {    if (process.env.NODE_ENV !== "production") {      if (series.primary.key === series.reference.key) {        console.warn(          `[Plotcn ComparisonArea]: Duplicate role key detected: primary and reference both use "${series.primary.key}". Comparison requires two distinct series keys.`        )      }    }  }, [series.primary.key, series.reference.key])  // Resolved colors: explicit prop > series config > default token  const resolvedPrimaryColor = primaryColor || series.primary.color || "var(--chart-1)"  const resolvedReferenceColor = referenceColor || series.reference.color || "var(--chart-2)"  const resolvedPrimaryOpacity = typeof primaryFillOpacity === "number" ? primaryFillOpacity : fillOpacity  const resolvedReferenceOpacity =    typeof referenceFillOpacity === "number" ? referenceFillOpacity : Math.max(0.08, resolvedPrimaryOpacity * 0.5)  // Interactive legend visibility state  const [visibleSeries, setVisibleSeries] = React.useState<Set<"primary" | "reference">>(    new Set(["primary", "reference"])  )  const handleToggleRole = React.useCallback((role: "primary" | "reference") => {    setVisibleSeries((prev) => {      const next = new Set(prev)      if (next.has(role)) {        next.delete(role)      } else {        next.add(role)      }      return next    })  }, [])  const handleRestoreRoles = React.useCallback(() => {    setVisibleSeries(new Set(["primary", "reference"]))  }, [])  // Normalized dataset  const normalizedData = React.useMemo(    () => normalizeComparisonData(data, xKey, series.primary.key, series.reference.key),    [data, xKey, series.primary.key, series.reference.key]  )  // Shared honest Y-domain  const sharedDomain = React.useMemo(    () => calculateSharedDomain(normalizedData, series.primary.key, series.reference.key, domain, visibleSeries),    [normalizedData, series.primary.key, series.reference.key, domain, visibleSeries]  )  // Locked inspection state  const [lockedIndex, setLockedIndex] = React.useState<number | null>(null)  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null)  const activeIndex = lockedIndex ?? hoverIndex  const activeDatum = React.useMemo(() => {    if (activeIndex === null || activeIndex < 0 || activeIndex >= normalizedData.length) {      return null    }    return (data[activeIndex] as TData) || null  }, [activeIndex, normalizedData.length, data])  React.useEffect(() => {    onActiveDatumChange?.(activeDatum)  }, [activeDatum, onActiveDatumChange])  // Keyboard navigation  const handleKeyDown = React.useCallback(    (e: React.KeyboardEvent) => {      if (!normalizedData.length) return      const maxIdx = normalizedData.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        }      }    },    [normalizedData.length, activeIndex, lockedIndex, lockableTooltip]  )  const handleChartClick = React.useCallback(    (state: any) => {      if (!lockableTooltip) return      if (state && state.activeTooltipIndex !== undefined) {        const clicked = Number(state.activeTooltipIndex)        setLockedIndex((prev) => (prev === clicked ? null : clicked))      } else if (lockedIndex !== null) {        setLockedIndex(null)      }    },    [lockableTooltip, lockedIndex]  )  /* ------------------------------------------------------------------------ */  /*  Early Return States                                                     */  /* ------------------------------------------------------------------------ */  if (loading) {    return (      <figure        role="region"        aria-label={title || "Comparison area loading"}        className={cn("plotcn-comparison-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 comparison data..." />      </figure>    )  }  if (unavailable) {    return (      <figure        role="region"        aria-label={title || "Comparison area unavailable"}        className={cn("plotcn-comparison-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 description={typeof unavailable === "string" ? unavailable : "Comparison 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 || "Comparison area error"}        className={cn("plotcn-comparison-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 }}      >        <ChartErrorState          title="Comparison Area Configuration Error"          description={errorDescription}          onRetry={onRetry}        />      </figure>    )  }  if (!normalizedData || normalizedData.length === 0) {    return (      <figure        role="region"        aria-label={title || "Comparison area empty"}        className={cn("plotcn-comparison-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 description="No comparison observations recorded." />      </figure>    )  }  const isCompact = typeof height === "number" ? height <= 260 : false  const allHidden = visibleSeries.size === 0  return (    <figure      role="region"      aria-label={title}      tabIndex={0}      onKeyDown={handleKeyDown}      className={cn(        "plotcn-comparison-area relative flex flex-col w-full overflow-hidden rounded-xl border border-white/10 bg-zinc-950 p-4 outline-none focus-visible:ring-2 focus-visible:ring-[var(--chart-focus)]",        className      )}      style={{ height, minHeight: typeof height === "number" ? height : 320 }}    >      {/* Live Screen Reader Announcement */}      <div className="sr-only" aria-live="polite" aria-atomic="true">        {activeDatum          ? `${activeDatum[xKey]}. ${series.primary.label}: ${              activeDatum[series.primary.key] !== null ? valueFormatter(activeDatum[series.primary.key] as number) : "Unavailable"            }, ${series.reference.label}: ${              activeDatum[series.reference.key] !== null ? valueFormatter(activeDatum[series.reference.key] as number) : "Unavailable"            }.`          : `${title}. Comparing ${series.primary.label} against ${series.reference.label} across ${normalizedData.length} observations.`}      </div>      {/* Semantic off-screen data table for accessibility (100% canvas height preserved for SVG) */}      <div className="sr-only">        <table>          <caption>{title} - Data Table</caption>          <thead>            <tr>              <th scope="col">{xKey}</th>              <th scope="col">{series.primary.label}</th>              <th scope="col">{series.reference.label}</th>              {showDelta && <th scope="col">Difference</th>}            </tr>          </thead>          <tbody>            {normalizedData.map((row, idx) => {              const pVal = row[series.primary.key]              const rVal = row[series.reference.key]              const hasP = isFiniteNumber(pVal)              const hasR = isFiniteNumber(rVal)              const deltaVal = hasP && hasR ? pVal - rVal : null              return (                <tr key={idx}>                  <td>{String(row[xKey] ?? "")}</td>                  <td>{hasP ? valueFormatter(pVal) : "Unavailable"}</td>                  <td>{hasR ? valueFormatter(rVal) : "Unavailable"}</td>                  {showDelta && (                    <td>                      {deltaVal !== null                        ? `${deltaVal > 0 ? "+" : deltaVal < 0 ? "−" : ""}${valueFormatter(Math.abs(deltaVal))}`                        : "Unavailable"}                    </td>                  )}                </tr>              )            })}          </tbody>        </table>      </div>      {/* All-series-hidden recovery state */}      {allHidden ? (        <div className="flex flex-1 flex-col items-center justify-center text-center p-6 gap-3">          <p className="text-sm font-medium text-muted-foreground">All comparison series are hidden.</p>          <button            type="button"            onClick={handleRestoreRoles}            className="rounded-md border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted transition-colors"          >            Restore comparison series          </button>        </div>      ) : (        <div className="flex-1 w-full min-h-0">          <ChartContainer className="h-full w-full">            <ResponsiveContainer              width="100%"              height="100%"              minWidth={0}              minHeight={0}              initialDimension={{ width: 600, height: typeof height === "number" ? height : 320 }}            >              <AreaChart                data={normalizedData}                onClick={handleChartClick}                onMouseMove={(e: any) => {                  if (lockedIndex === null && e?.activeTooltipIndex !== undefined) {                    setHoverIndex(Number(e.activeTooltipIndex))                  }                }}                onMouseLeave={() => {                  if (lockedIndex === null) setHoverIndex(null)                }}                margin={{ top: 12, right: 16, bottom: 8, left: 4 }}              >                {showGrid && (                  <CartesianGrid                    strokeDasharray="3 3"                    stroke="var(--chart-grid)"                    vertical={false}                    strokeOpacity={0.7}                  />                )}                {showXAxis && (                  <XAxis                    dataKey={xKey as any}                    stroke="var(--chart-axis, #a1a1aa)"                    tick={{ fill: "var(--chart-axis, #a1a1aa)", fontSize: isCompact ? 10 : 11 }}                    tickLine={false}                    axisLine={{ stroke: "var(--chart-axis-line, rgba(255,255,255,0.12))", strokeOpacity: 0.5 }}                    tickFormatter={xFormatter}                  />                )}                {showYAxis && (                  <YAxis                    domain={sharedDomain as any}                    stroke="var(--chart-axis, #a1a1aa)"                    tick={{ fill: "var(--chart-axis, #a1a1aa)", fontSize: isCompact ? 10 : 11 }}                    tickLine={false}                    axisLine={false}                    tickFormatter={valueFormatter}                    width={isCompact ? 40 : 50}                  />                )}              <Tooltip                isAnimationActive={false}                cursor={{                  stroke: lockedIndex !== null ? selectionColor : "var(--chart-crosshair)",                  strokeWidth: 1.5,                  strokeDasharray: "4 4",                }}                content={                  <ComparisonAreaTooltipContent                    primaryKey={series.primary.key}                    referenceKey={series.reference.key}                    primaryLabel={series.primary.label}                    referenceLabel={series.reference.label}                    primaryColor={resolvedPrimaryColor}                    referenceColor={resolvedReferenceColor}                    valueFormatter={valueFormatter}                    showDelta={showDelta}                    deltaType={deltaType}                    invertDelta={invertDelta}                    isCompact={isCompact}                    isLocked={lockedIndex !== null}                    onUnlock={() => setLockedIndex(null)}                  />                }              />              {/* 1. Reference Area (rendered FIRST, sits underneath with dashed stroke and quieter fill) */}              <Area                type={curve}                dataKey={series.reference.key as any}                name={series.reference.label}                stroke={resolvedReferenceColor}                strokeWidth={1.5}                strokeDasharray="4 3"                fill={resolvedReferenceColor}                fillOpacity={resolvedReferenceOpacity}                connectNulls={missingValuePolicy === "connect"}                isAnimationActive={motion !== false && !isReducedMotion}                animationDuration={typeof motion === "object" && motion.duration ? motion.duration : 350}                hide={!visibleSeries.has("reference")}                dot={false}                activeDot={{                  r: 4.5,                  fill: resolvedReferenceColor,                  stroke: "var(--background, #09090b)",                  strokeWidth: 2,                }}              />              {/* 2. Primary Area (rendered SECOND, sits on top with solid boundary and stronger fill) */}              <Area                type={curve}                dataKey={series.primary.key as any}                name={series.primary.label}                stroke={resolvedPrimaryColor}                strokeWidth={2}                fill={resolvedPrimaryColor}                fillOpacity={resolvedPrimaryOpacity}                connectNulls={missingValuePolicy === "connect"}                isAnimationActive={motion !== false && !isReducedMotion}                animationDuration={typeof motion === "object" && motion.duration ? motion.duration : 350}                hide={!visibleSeries.has("primary")}                dot={false}                activeDot={{                  r: 5.5,                  fill: resolvedPrimaryColor,                  stroke: "var(--background, #09090b)",                  strokeWidth: 2,                }}              />            </AreaChart>          </ResponsiveContainer>        </ChartContainer>      </div>      )}      {/* Series Legend */}      {showLegend && (        <ComparisonAreaLegend          primaryLabel={series.primary.label}          referenceLabel={series.reference.label}          primaryColor={resolvedPrimaryColor}          referenceColor={resolvedReferenceColor}          visibleSeries={visibleSeries}          onToggle={handleToggleRole}          interactive={interactiveLegend}          isCompact={isCompact}        />      )}    </figure>  )}