010 / RECHARTS / LINE

Forecast Line

Recharts

Observed-versus-forecast time-series with explicit prediction styling, truthful boundary transitions, and optional uncertainty range treatment.

SPEC
#010
ENGINE
Recharts
FAMILY
Line
RENDERER
svg
STATUS
preview

Installation

PLOTCN/REGISTRY/LINE-FORECAST/SOURCE
pnpm dlx shadcn@latest add @plotcn/line-forecast

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

Forecast Line is an analytical time-series component engineered around the fundamental principle of epistemic separation: strictly distinguishing what has actually occurred from what is predicted to happen next.

The central analytical question answered by this chart is:

"What has actually happened, what is predicted to happen next, and how uncertain is that prediction?"

Forecast Line must never blur the semantic difference between observed data, predicted data, and uncertainty around predictions.

EPISTEMIC SEPARATIONObserved vs. Predicted Architecture

Observed History Transitioning into Predicted Future with Uncertainty

Actual (Solid)
Forecast (Dashed)
Range Band
Phase 1: Observed History (Definite Facts)Phase 2: Forecast Horizon (Prediction & Uncertainty)Apr (Transition)JanFebMarMayJunJulSolid Actual LineUncertainty Band [L, U]Dashed Forecast Line
Truthful Principle: Observed data is solid; predicted trajectory is dashed.
Confidence area renders only where valid bounds exist
TSX
import { ForecastLine } from "@/components/charts/recharts/line-forecast"const activeUserData = [  { month: "Jan", actual: 82, forecast: null, lower: null, upper: null },  { month: "Feb", actual: 91, forecast: null, lower: null, upper: null },  { month: "Mar", actual: 104, forecast: 104, lower: 104, upper: 104 },  { month: "Apr", actual: null, forecast: 112, lower: 103, upper: 121 },  { month: "May", actual: null, forecast: 121, lower: 108, upper: 134 },  { month: "Jun", actual: null, forecast: 130, lower: 114, upper: 146 },]export function MonthlyUserForecast() {  return (    <ForecastLine      data={activeUserData}      xKey="month"      series={{        actualKey: "actual",        forecastKey: "forecast",        lowerKey: "lower",        upperKey: "upper",        actualLabel: "Actual Users",        forecastLabel: "Forecast Users",        confidenceLabel: "Forecast Range",        valueFormatter: (v) => `${v.toLocaleString()}k`,      }}      showLegend      lockableTooltip      showGrid    />  )}

Line-Family Positioning

Plotcn provides specialized line chart components for distinct analytical and operational roles. It is essential to select the component that accurately matches data semantics:

Component Number Primary Analytical Specialty
ComponentNumberPrimary Analytical Specialty
Signal Line001Single continuous quantitative trend
Pulse Line002Live operational telemetry with rolling window and pulse marker
Twinline Compare003Explicit two-role comparison (primary signal vs. reference baseline)
Range Line004Continuous trend plus uncertainty envelope / confidence interval across all observations
Step Signal005Discrete state transitions and quota tier changes
Milestone Line006Trend line annotated with sparse releases and milestones
Threshold Line007Quantitative trend evaluated against operational limits and bands
Focus Line008High-precision single-series observation scrubbing and locked pins
Multi-Signal Line009Multiple peer series with deterministic identity and interactive legend
Forecast Line010Observed history transitioning into predicted future with uncertainty

Critical Distinction — Forecast vs. Twinline Compare

In Twinline Compare, both lines (Current and Previous) are typically real historical peer series. In Forecast Line, the observed series and predicted series represent different epistemic realities: facts versus projections. Forecast Line uses distinct line styles (solid vs. dashed), independent confidence treatment, and differentiated tooltip semantics.

Critical Distinction — Forecast vs. Range Line

In Range Line, the interval envelope belongs to the primary series throughout the entire observation domain. In Forecast Line, uncertainty typically begins only where prediction begins. Historical facts have zero uncertainty unless explicitly documented in historical records.

Forecast Model & Principles

Forecast Line enforces strict analytical boundaries:

  1. Plotcn Does Not Calculate Forecasts: Forecast Line receives predicted values and bounds directly from your application or forecasting backend (e.g., ARIMA, Prophet, Holt-Winters, ML service). Plotcn never extrapolates lines or generates predictions.
  2. Non-Color Distinction is Mandatory: Observed data renders as a solid line (strokeWidth={2}). Predicted data renders as a dashed line (strokeDasharray="5 5"). This ensures clear separation under any color scheme, including Monochrome or high-contrast themes.
  3. Restrained Presentation: Forecast Line avoids distracting visual effects such as animated dash offset ("marching ants") or glowing projections. Predictions are uncertain, not "loading".

Transition Boundary Semantics

The forecast transition marks the chronological boundary where observed facts hand off to predicted values.

BOUNDARY SEMANTICSTransition Topologies

How Plotcn Truthfully Resolves the Forecast Transition Boundary

Model A: Shared BridgeContinuous

The final actual observation shares the exact coordinate and value with the first forecast observation.

Mar (Bridge)
✓ Seamless visual handoff without fabricating data.
Model B: Separate GapDiscontinuous

Actual history ends at Mar, and forecast begins at Apr with no shared bridge point.

Honest Gap
✓ Honest separation; never secretly bridges missing data.
Model C: Overlap PeriodEvaluation

Both actual and forecast exist across multiple intervals for backtesting or model evaluation.

Coexisting Signals
✓ Both signals rendered faithfully without hiding actuals.

Forecast Line deterministically handles three data arrangements:

A. Shared Transition Bridge (Continuous Handoff)

TEXT
Month     Actual    Forecast    Lower    UpperMar       104       104         104      104Apr       —         112         103      121

The boundary observation (Mar) contains matching values for both actual and forecast. This creates a clean, continuous transition across the timeline without gaps.

B. Discontinuous Separation (Truthful Gap)

TEXT
Month     Actual    Forecast    Lower    UpperMar       104       —           —        —Apr       —         112         103      121

Observed history ends before prediction starts. Plotcn visually preserves this gap. Plotcn never fabricates an artificial bridge point just to make the chart look connected.

C. Overlapping Actual & Forecast (Evaluation / Backtesting)

TEXT
Month     Actual    Forecast    Lower    UpperMar       104       98          90       106Apr       112       105         97       113

When comparing historical model performance against observed outcomes, actual and forecast lines render concurrently, each retaining its respective visual style (solid vs. dashed).

Confidence & Uncertainty Treatment

When lowerKey and upperKey are configured, Forecast Line renders a translucent prediction band behind the forecast line:

  • Supplied Bounds Only: Plotcn never calculates synthetic confidence bounds (e.g., ±10% or standard errors). All bounds are supplied data.
  • Band Appears Only Where Bounds Exist: If bounds are missing or null for any row, the uncertainty band cleanly gaps at that point.
  • Invalid Bounds Safeguard: If lower > upper, the interval at that observation is treated as invalid. The band is omitted locally. Plotcn never silently swaps lower and upper bounds.
  • No Clamping: If the forecast value falls outside the [lower, upper] interval, Plotcn renders the values truthfully without artificial clamping.
  • Neutral Terminology: Unless customized via confidenceLabel, default language refers to the range neutrally as "Forecast range" rather than making unverified statistical claims like "95% confidence interval".

Data & Series Contracts

Forecast Line accepts a single ordered array of data records:

TypeScript
type ForecastLineData = {  month: string | Date  actual: number | null  forecast: number | null  lower?: number | null  upper?: number | null}

Series Configuration Object

TypeScript
interface ForecastLineSeries<TData> {  actualKey: NumericKeyOf<TData>  forecastKey: NumericKeyOf<TData>  lowerKey?: NumericKeyOf<TData>  upperKey?: NumericKeyOf<TData>  actualLabel?: string  forecastLabel?: string  confidenceLabel?: string  valueFormatter?: (value: number) => string}

Installation

Install Forecast Line directly into your project using the shadcn CLI:

PLOTCN/REGISTRY/LINE-FORECAST/SOURCE
pnpm dlx shadcn@latest add @plotcn/line-forecast

Checking public registry…

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

Copied as source into your project (requires recharts).

Component Props

Property Type Default Required Description
PropertyTypeDefaultRequiredDescription
datareadonly TData[][]RequiredReadonly array of observation records. Caller data is never mutated.
xKeykeyof TData & stringRequiredProperty name for horizontal domain coordinates (e.g., month, date).
seriesForecastLineSeries<TData>RequiredSemantic series configuration mapping observed, forecast, and bound keys.
heightnumber | string320OptionalContainer height in pixels or standard CSS dimension strings.
curve"monotone" | "linear" | "step""monotone"OptionalCurve interpolation algorithm shared between actual, forecast, and bounds.
domain[number, number] | ["auto", "auto"]["auto", "auto"]OptionalVertical Y scale domain. Automatically encloses actual, forecast, and range bounds.
actualColorstring"var(--chart-1)"OptionalColor for observed history line, markers, and tooltip identity.
forecastColorstring"var(--chart-2)"OptionalColor for predicted future line, markers, and tooltip identity.
confidenceColorstring"var(--chart-2)"OptionalColor for uncertainty band fill.
confidenceOpacitynumber0.15OptionalOpacity for the uncertainty range band (0 to 1).
selectionColorstring"var(--chart-selection)"OptionalAccent color for the inspection crosshair and locked datum status indicator.
showGridbooleantrueOptionalWhether to render horizontal Cartesian grid reference lines.
showLegendbooleantrueOptionalWhether to render the series identity legend with actual stroke samples.
lockableTooltipbooleantrueOptionalEnables persistent tooltip locking via click, tap, or Enter/Space.
missingValuePolicy"gap" | "carry""gap"OptionalVisual treatment of missing values: honest visual break or forward carry.
animation"draw" | "fade" | "none""draw"OptionalEntry reveal animation. Automatically bypassed when reduced motion is preferred.
valueFormatter(value: number) => stringn.toLocaleString()OptionalDefault formatter for tooltip metric values and vertical scale ticks.
xFormatter(value: string | number) => stringStringOptionalCustom formatter for horizontal domain tick labels.
titlestring"Forecast Line Chart"OptionalAccessible title for assistive technologies and screen readers.
descriptionstringundefinedOptionalAccessible description detailing forecast horizon and keyboard shortcuts.

Keyboard Navigation Reference

Forecast Line provides full single-entry-point keyboard inspection across the unified timeline:

Key Control Target Action
KeyControl TargetAction
/ ArrowRightPlot InspectionInspect next chronological observation (observed or forecast)
/ ArrowLeftPlot InspectionInspect previous chronological observation
HomePlot InspectionJump inspection to the first observed observation
EndPlot InspectionJump inspection to the latest forecast observation
Enter or SpacePlot InspectionLock or unlock persistent inspection at the active coordinate
EscapePlot InspectionDismiss locked inspection and release pinned tooltip
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):
<ForecastLine
  data={data}
  xKey="date"
  series={{
    actualKey: "actual",
    forecastKey: "forecast",
    lowerKey: "lower",
    upperKey: "upper",
  }}
/>
All Properties (21)
Component properties
PropertyTypeDefaultRequiredDescription
dataReq
readonly TData[][]Yes

Readonly array of observation records. Caller data is never mutated.

Best for: Primary dataset

xKeyReq
keyof TData & stringYes

Property name for horizontal domain coordinates (e.g., month, date).

Best for: Domain mapping

ForecastLineSeries<TData>Yes

Semantic configuration object specifying actualKey, forecastKey, optional lowerKey, upperKey, and labels.

Best for: Semantic role mapping

string"var(--chart-1, #3b82f6)"No

Stroke color for the solid observed historical line and past observation markers.

string"var(--chart-2, #10b981)"No

Stroke color for the dashed predicted forecast line and future observation markers.

string"var(--chart-2, #10b981)"No

Fill color for the shaded forecast uncertainty and prediction interval band.

number0.18No

Fill opacity applied to the uncertainty prediction band (0.0 to 1.0).

string"var(--chart-selection, #f59e0b)"No

Accent color for the locked vertical crosshair and persistent inspection selection.

number | string320No

Container height in pixels or standard CSS dimension strings.

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

Curve interpolation algorithm applied consistently across actual and forecast lines.

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

Vertical Y scale range calculated safely across actual, forecast, and confidence bounds.

booleantrueNo

Whether to render subtle horizontal Cartesian grid reference lines.

booleantrueNo

Whether to render the series identity legend showing stroke samples.

booleantrueNo

Enables interactive button controls in the legend to toggle actual, forecast, or confidence visibility.

booleantrueNo

Enables persistent tooltip locking via click, tap, or Enter/Space.

"gap" | "carry""gap"No

Visual treatment of missing values: honest visual break or forward carry.

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

Entry reveal animation. Bypassed automatically when reduced motion is preferred.

(value: number) => stringn.toLocaleString()No

Default formatter for tooltip metric values and vertical scale ticks.

(value: string | number) => stringStringNo

Custom formatter for horizontal domain tick labels.

string"Forecast Line Chart"No

Accessible title for assistive technologies and screen readers.

stringundefinedNo

Accessible description detailing historical duration, forecast horizon, and keyboard shortcuts.

04 / Cookbook & States

Component Variants & Edge States

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

Monthly Active Users Forecast

Four months of observed historical metrics transitioning at April into a three-month forecast with supplied confidence bounds.

<ForecastLine
  data={forecastData}
  xKey="month"
  series={{
    actualKey: "actual",
    forecastKey: "forecast",
    lowerKey: "lower",
    upperKey: "upper",
  }}
  lockableTooltip
/>

Custom Brand Color Encodings

Explicit brand colors assigned to actual history and forecast trajectory while preserving the solid vs. dashed non-color distinction.

<ForecastLine
  data={forecastData}
  xKey="month"
  series={seriesConfig}
  actualColor="#18181b"
  forecastColor="#7c3aed"
  confidenceColor="#c4b5fd"
/>

Discontinuous Transition Gap

Demonstrates truthful separation when history ends before forecast begins without inventing artificial bridge data.

<ForecastLine
  data={gapData}
  xKey="month"
  series={{
    actualKey: "actual",
    forecastKey: "forecast",
  }}
/>

Missing Uncertainty Bounds

Demonstrates truthful confidence treatment: when lower or upper bounds are missing, the band gaps without fabricating fake intervals.

<ForecastLine
  data={partialRangeData}
  xKey="month"
  series={{
    actualKey: "actual",
    forecastKey: "forecast",
    lowerKey: "lower",
    upperKey: "upper",
  }}
/>
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

Forecast Line adapts legend presentation and horizontal axis ticks based on container width. The solid vs. dashed distinction and confidence range area remain visible across all viewports.

Desktop
>= 1024px

Full horizontal legend with stroke samples, complete X-axis ticks, and spacious shared tooltip readouts.

Tablet
640px - 1023px

Wrapped multi-row legend, thinned categorical ticks, and synchronized nearest-X inspection.

Mobile
< 640px

Compact wrapped controls with 32px touch targets, pan-y page scroll safety, and viewport-clamped tooltip positioning.

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, and Escape shortcuts. Screen readers announce factual observation metrics and distinguish historical actuals from predicted forecasts.

Semantic Role & Landmark

Container mounts as region with explicit assistive label.

Color-Independent Legibility

Observed history is rendered as a solid stroke, forecast is rendered as a dashed stroke, and uncertainty is rendered as a filled area. The distinction remains fully readable in monochrome.

Screen Reader Summary

Embeds visually hidden summary (.sr-only) declaring: “Announces domain time, actual observed value, predicted forecast value, and supplied forecast range factually without speculative probability claims.

Reduced Motion Support

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

Keyboard Interaction Model
Keyboard interaction model
KeyAction
ArrowRightInspect next observation along the timeline.
ArrowLeftInspect previous observation along the timeline.
HomeJump inspection to the first historical observation.
EndJump inspection to the final forecast observation.
Enter / SpaceLock or unlock the currently active inspection coordinate.
EscapeRelease locked selection and dismiss active tooltip.
TabMove focus to interactive legend series toggle buttons.

Data Safety Checklist

  • ✓ No Automatic Prediction: Plotcn visualizes forecast data supplied by the consumer; it never runs automatic regressions or forecasts.
  • ✓ Missing Values Never Become Zero: Missing actual, forecast, or bound fields remain null and create honest gaps (null ≠ 0).
  • ✓ Missing Confidence Bounds Gap: Missing bounds produce a gap in the confidence band rather than synthetic bounds.
  • ✓ No Silent Bound Swapping: Observations with lower > upper invalidate the band locally rather than swapping values.
  • ✓ No Bridge Fabrication: If observed history ends before forecast starts, the gap is truthfully rendered.
  • ✓ Safe Domain Calculation: Vertical domain includes all valid values across actual, forecast, lower, and upper series to prevent clipping.
  • ✓ Immutability: Consumer data arrays are never mutated or sorted in place.
  • ✓ Non-Color Encoding: Actual renders as a solid line and forecast as a dashed line, preserving epistemic meaning in monochrome.
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
ForecastLine(Root figure element with keyboard navigation and ARIA accessibility shell)
├──ChartContainer[Responsive container wrapper]

Handles container dimension measurement and CSS token scoping

├──ComposedChart[Recharts Cartesian SVG coordinator]

Coordinates coordinate scales, Cartesian grid, Area, and multiple Line elements

└──Legend Controls[Responsive interactive legend controls]

Semantic button controls allowing users to toggle actual, forecast, and confidence visibility

Involved Source Files & Registry Assets
registry/recharts/line-forecast.tsx
Complete Forecast Line component with observed vs forecast rendering, confidence band, and truthful missing handling
registry/recharts/line-forecast.tsx
"use client"import * as React from "react"import {  ResponsiveContainer,  ComposedChart,  Line,  Area,  XAxis,  YAxis,  Tooltip,  CartesianGrid,  ReferenceLine,} from "recharts"import { HugeiconsIcon } from "@hugeicons/react"import { LockKeyIcon, ViewIcon, ViewOffSlashIcon } from "@hugeicons/core-free-icons"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"/* -------------------------------------------------------------------------- *//*  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] & string/** * Semantic series descriptor for Forecast Line. * Forecast Line enforces semantic roles: Observed Actual, Predicted Forecast, * and Optional Uncertainty bounds (lower & upper). */export interface ForecastLineSeries<TData extends Record<string, unknown> = Record<string, unknown>> {  /** Property key for observed historical values */  actualKey: NumericKeyOf<TData>  /** Property key for predicted/forecast values */  forecastKey: NumericKeyOf<TData>  /** Optional property key for lower confidence/prediction interval bound */  lowerKey?: NumericKeyOf<TData>  /** Optional property key for upper confidence/prediction interval bound */  upperKey?: NumericKeyOf<TData>  /** Display label for observed values (default: "Actual") */  actualLabel?: string  /** Display label for forecast values (default: "Forecast") */  forecastLabel?: string  /** Display label for the uncertainty range (default: "Forecast range") */  confidenceLabel?: string  /** Custom formatter for metric values */  valueFormatter?: (value: number) => string}export interface NormalizedForecastDatum<TData = any> {  __x: string | number  __index: number  __raw: TData  __actual: number | null  __forecast: number | null  __lower: number | null  __upper: number | null  __range: [number, number] | null  __isBridge: boolean  __isForecastPhase: boolean}export interface ForecastActiveDatum<  TData extends Record<string, unknown> = Record<string, unknown>,  XVal extends string | number = string | number> {  index: number  x: XVal  raw: TData  actual: number | null  forecast: number | null  lower: number | null  upper: number | null  isBridge: boolean  isForecastPhase: boolean  isLocked: boolean}export interface ForecastLineProps<  TData extends Record<string, unknown> = Record<string, unknown>,  XVal extends string | number = string | number> {  /** Readonly array of observation records. Caller data is never mutated. */  data: readonly TData[]  /** Property key for horizontal domain coordinates (e.g., month, date, time). */  xKey: keyof TData & string  /** Semantic forecast series configuration containing actualKey, forecastKey, and optional bounds. */  series: ForecastLineSeries<TData>  /** Container height in pixels or CSS dimension string. (default: 320) */  height?: number | string  /** Curve interpolation: "monotone" | "linear" | "step". (default: "monotone") */  curve?: "linear" | "monotone" | "step"  /** Explicit Y-axis numeric domain, or "auto" safe domain. (default: "auto") */  domain?: [number, number] | ["auto", "auto"] | "auto"  /** Explicit color override for observed history. (default: var(--chart-1, #3b82f6)) */  actualColor?: string  /** Explicit color override for forecast predictions. (default: var(--chart-2, #10b981)) */  forecastColor?: string  /** Explicit color override for uncertainty range band. (default: derived from forecastColor) */  confidenceColor?: string  /** Fill opacity for the confidence band area (0.0 to 1.0). (default: 0.18) */  confidenceOpacity?: number  /** Accent color for locked crosshair and selection markers. (default: var(--chart-selection, #f59e0b)) */  selectionColor?: string  /** Whether to render subtle horizontal background reference gridlines. (default: true) */  showGrid?: boolean  /** Whether to render the horizontal category scale. (default: true) */  showXAxis?: boolean  /** Whether to render the vertical numeric scale. (default: true) */  showYAxis?: boolean  /** Whether to render the series identity legend. (default: true) */  showLegend?: boolean  /** Whether legend items can be clicked/keyboard-activated to toggle role visibility. (default: true) */  interactiveLegend?: boolean  /** Whether clicking or pressing Enter/Space pins the currently inspected X datum. (default: true) */  lockableTooltip?: boolean  /** Default observation index to pin/lock on initial mount. (default: null) */  defaultLockedIndex?: number | null  /** Visual policy for missing observations: 'gap' leaves breaks, 'carry' forward-fills. (default: "gap") */  missingValuePolicy?: "gap" | "carry"  /** Animation mode: "draw" | "fade" | "none". (default: "draw") */  animation?: "draw" | "fade" | "none"  /** Motion toggle or configuration object. Respects prefers-reduced-motion. */  motion?: boolean | { duration?: number }  /** Custom formatter for Y-axis scale numbers and default tooltip metric values. */  valueFormatter?: (value: number) => string  /** Custom formatter for X-axis coordinate labels. */  xFormatter?: (value: string | number) => string  /** Callback fired whenever the active inspected observation changes. */  onActiveDatumChange?: (datum: ForecastActiveDatum<TData, XVal> | null) => void  /** Optional heading announced to assistive technologies. */  title?: string  /** Optional descriptive summary announced to assistive technologies. */  description?: string  /** Optional CSS class name passed to the root figure element. */  className?: string  /** Display neutral skeleton loading state. */  loading?: boolean  /** Display actionable error banner. */  error?: Error | string | null  /** Display metric unavailability notice. */  unavailable?: boolean | string | null}/* -------------------------------------------------------------------------- *//*  Validation & Normalization Helpers                                        *//* -------------------------------------------------------------------------- */export function isFiniteNumber(val: unknown): val is number {  return typeof val === "number" && Number.isFinite(val)}/** * Normalizes input observations into structured format: * - Extracts actual, forecast, lower, upper independently. * - Handles missing observations truthfully (null != 0). * - Identifies transition bridge observations (where actual and forecast coexist). * - Validates confidence intervals (if lower > upper, interval is invalidated locally; never silently swapped). * - Immutability: caller data is never mutated. */export function normalizeForecastData<TData extends Record<string, unknown>>(  data: readonly TData[],  xKey: keyof TData & string,  series: ForecastLineSeries<TData>,  policy: "gap" | "carry" = "gap"): NormalizedForecastDatum<TData>[] {  if (!Array.isArray(data) || data.length === 0) return []  let lastActual: number | null = null  let lastForecast: number | null = null  // First pass: resolve basic values  const normalized: NormalizedForecastDatum<TData>[] = data.map((d, idx) => {    const rawX = d[xKey]    const xVal = typeof rawX === "string" || typeof rawX === "number" ? rawX : String(rawX ?? "")    const rawActual = d[series.actualKey]    let actualVal = isFiniteNumber(rawActual) ? rawActual : null    const rawForecast = d[series.forecastKey]    let forecastVal = isFiniteNumber(rawForecast) ? rawForecast : null    if (policy === "carry") {      if (actualVal !== null) lastActual = actualVal      else if (lastActual !== null) actualVal = lastActual      if (forecastVal !== null) lastForecast = forecastVal      else if (lastForecast !== null) forecastVal = lastForecast    }    const rawLower = series.lowerKey ? d[series.lowerKey] : null    const lowerVal = isFiniteNumber(rawLower) ? rawLower : null    const rawUpper = series.upperKey ? d[series.upperKey] : null    const upperVal = isFiniteNumber(rawUpper) ? rawUpper : null    // Range is valid only if both bounds exist and lower <= upper    let range: [number, number] | null = null    if (lowerVal !== null && upperVal !== null) {      if (lowerVal <= upperVal) {        range = [lowerVal, upperVal]      } else {        // Invalid bound: lower > upper. Do NOT silently swap!        if (process.env.NODE_ENV !== "production") {          console.warn(            `[ForecastLine] Invalid confidence bound at X="${xVal}": lower (${lowerVal}) > upper (${upperVal}). Interval omitted.`          )        }        range = null      }    }    const isBridge = actualVal !== null && forecastVal !== null    return {      __x: xVal,      __index: idx,      __raw: d,      __actual: actualVal,      __forecast: forecastVal,      __lower: lowerVal,      __upper: upperVal,      __range: range,      __isBridge: isBridge,      __isForecastPhase: forecastVal !== null && actualVal === null,    }  })  return normalized}/** * Calculates a Cartesian Y-domain covering all valid values of actual, forecast, lower, and upper. */export function calculateForecastDomain(  normalized: readonly NormalizedForecastDatum<any>[],  explicitDomain?: [number, number] | ["auto", "auto"] | "auto",  rolesVisible = { actual: true, forecast: true, confidence: true }): [number, number] {  if (    Array.isArray(explicitDomain) &&    typeof explicitDomain[0] === "number" &&    typeof explicitDomain[1] === "number" &&    Number.isFinite(explicitDomain[0]) &&    Number.isFinite(explicitDomain[1])  ) {    return explicitDomain  }  const values: number[] = []  for (const row of normalized) {    if (rolesVisible.actual && row.__actual !== null) {      values.push(row.__actual)    }    if (rolesVisible.forecast && row.__forecast !== null) {      values.push(row.__forecast)    }    if (rolesVisible.confidence && row.__range !== null) {      values.push(row.__range[0], row.__range[1])    }  }  if (values.length === 0) {    return [0, 100]  }  const min = Math.min(...values)  const max = Math.max(...values)  if (min === max) {    if (min === 0) return [-10, 10]    const delta = Math.abs(min) * 0.15 || 10    return [Math.floor(min - delta), Math.ceil(max + delta)]  }  const span = max - min  const pad = span * 0.08  return [Math.floor(min - pad), Math.ceil(max + pad)]}/* -------------------------------------------------------------------------- *//*  Synchronized Forecast Tooltip Content                                     *//* -------------------------------------------------------------------------- */interface ForecastTooltipContentProps {  active?: boolean  payload?: readonly any[]  label?: React.ReactNode  activeX: string | number | null  activeDatum: ForecastActiveDatum | null  actualLabel: string  forecastLabel: string  confidenceLabel: string  actualColor: string  forecastColor: string  confidenceColor: string  rolesVisible: { actual: boolean; forecast: boolean; confidence: boolean }  valueFormatter?: (value: number) => string  xFormatter?: (value: string | number) => string  lockableTooltip?: boolean  isLocked?: boolean  selectionColor?: string}function ForecastTooltipContent({  active,  payload,  label: _label,  activeX,  activeDatum,  actualLabel,  forecastLabel,  confidenceLabel,  actualColor,  forecastColor,  confidenceColor,  rolesVisible,  valueFormatter,  xFormatter,  lockableTooltip,  isLocked = false,  selectionColor = "var(--chart-selection, #f59e0b)",}: ForecastTooltipContentProps) {  const payloadRow = payload?.[0]?.payload as NormalizedForecastDatum | undefined  const hasPayload = Boolean(payloadRow)  if (!activeDatum && (!active || !hasPayload)) {    return null  }  const rawX = activeDatum ? activeDatum.x : (payloadRow?.__x ?? activeX ?? "")  const xDisplay = xFormatter ? xFormatter(rawX) : String(rawX)  const lockedState = activeDatum ? activeDatum.isLocked : isLocked  const fmt = valueFormatter ?? ((n: number) => n.toLocaleString())  const actualVal = activeDatum ? activeDatum.actual : (payloadRow ? payloadRow.__actual : null)  const forecastVal = activeDatum ? activeDatum.forecast : (payloadRow ? payloadRow.__forecast : null)  const lowerVal = activeDatum ? activeDatum.lower : (payloadRow ? payloadRow.__lower : null)  const upperVal = activeDatum ? activeDatum.upper : (payloadRow ? payloadRow.__upper : null)  const hasRange = lowerVal !== null && upperVal !== null && lowerVal <= upperVal  const isBridge = actualVal !== null && forecastVal !== null  const isCustomHex = typeof selectionColor === "string" && selectionColor.startsWith("#")  return (    <div      className={cn(        "rounded-xl border bg-zinc-950/95 p-3.5 shadow-2xl backdrop-blur-md min-w-[210px] max-w-[290px] text-left transition-all duration-150 pointer-events-none select-none z-50",        lockedState          ? "border-amber-500/40 shadow-[0_12px_32px_-4px_rgba(0,0,0,0.6),0_0_16px_rgba(245,158,11,0.15)] ring-1 ring-amber-500/25"          : "border-white/[0.12] ring-1 ring-white/[0.04]"      )}      style={        lockedState && isCustomHex          ? {              borderColor: `${selectionColor}66`,              boxShadow: `0 12px 32px -4px rgba(0,0,0,0.6), 0 0 16px ${selectionColor}26, 0 0 0 1px ${selectionColor}40`,            }          : undefined      }    >      {/* Header: Observation Coordinate + Optional Lock Badge */}      <div className="flex items-center justify-between gap-2 border-b border-white/[0.08] pb-2 mb-2.5">        <div className="flex items-center gap-1.5 min-w-0">          <span className="font-mono text-xs font-semibold text-zinc-200 tracking-wide truncate">            {xDisplay}          </span>          {isBridge && (            <span className="text-[9px] font-mono uppercase px-1.5 py-0.5 rounded bg-white/10 text-zinc-300 font-medium">              Transition            </span>          )}        </div>        {lockedState && (          <span            className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-mono font-semibold tracking-tight border bg-amber-500/15 border-amber-500/40 text-amber-300 dark:bg-amber-400/15 dark:border-amber-400/40 dark:text-amber-200 shadow-2xs"            style={              isCustomHex                ? {                    backgroundColor: `${selectionColor}20`,                    borderColor: `${selectionColor}55`,                    color: selectionColor,                  }                : undefined            }          >            <HugeiconsIcon icon={LockKeyIcon} size={11} strokeWidth={2.2} className="shrink-0" />            Locked          </span>        )}      </div>      {/* Semantic Rows in Canonical Order: Actual -> Forecast -> Forecast Range */}      <div className="space-y-1.5">        {/* Actual Row */}        {rolesVisible.actual && (          <div className="flex items-center justify-between gap-3 text-xs">            <div className="flex items-center gap-2 min-w-0">              <span                className="w-3.5 h-1 rounded-full shrink-0 ring-1 ring-white/20 shadow-xs"                style={{ backgroundColor: actualColor }}              />              <span className="text-zinc-300 font-sans truncate text-xs font-medium">                {actualLabel}              </span>            </div>            <span className="font-mono text-xs font-bold text-white shrink-0 tabular-nums">              {actualVal !== null ? fmt(actualVal) : "—"}            </span>          </div>        )}        {/* Forecast Row */}        {rolesVisible.forecast && (          <div className="flex items-center justify-between gap-3 text-xs">            <div className="flex items-center gap-2 min-w-0">              <svg width={14} height={4} className="shrink-0 overflow-visible" aria-hidden="true">                <line x1={0} y1={2} x2={14} y2={2} stroke={forecastColor} strokeWidth={2} strokeDasharray="3 2" />              </svg>              <span className="text-zinc-300 font-sans truncate text-xs font-medium">                {forecastLabel}              </span>            </div>            <span className="font-mono text-xs font-bold text-white shrink-0 tabular-nums">              {forecastVal !== null ? fmt(forecastVal) : "—"}            </span>          </div>        )}        {/* Confidence Interval Row */}        {rolesVisible.confidence && (lowerVal !== null || upperVal !== null) && (          <div className="flex items-center justify-between gap-3 text-xs pt-1 border-t border-white/[0.06]">            <div className="flex items-center gap-2 min-w-0">              <span                className="size-2 rounded-xs shrink-0 border"                style={{                  backgroundColor: `${confidenceColor}33`,                  borderColor: confidenceColor,                }}              />              <span className="text-zinc-400 font-sans truncate text-[11px]">                {confidenceLabel}              </span>            </div>            <span className="font-mono text-xs font-medium text-zinc-300 shrink-0 tabular-nums">              {hasRange ? `${fmt(lowerVal)} – ${fmt(upperVal)}` : "—"}            </span>          </div>        )}      </div>      {/* Lock Interaction Hint */}      {lockableTooltip && (        <div className="mt-2.5 pt-2 border-t border-white/[0.08] flex items-center justify-between text-[10px] font-mono text-zinc-400">          {lockedState ? (            <span className="flex items-center gap-1.5">              <kbd className="inline-flex items-center justify-center px-1.5 py-0.5 rounded bg-zinc-800/90 border border-white/15 text-[9px] font-mono font-semibold text-zinc-200 shadow-2xs">                Esc              </kbd>              <span className="text-zinc-400">or click to release</span>            </span>          ) : (            <span className="flex items-center gap-1.5 text-zinc-500">              <kbd className="inline-flex items-center justify-center px-1.5 py-0.5 rounded bg-zinc-800/60 border border-white/10 text-[9px] font-mono font-medium text-zinc-400">                Click              </kbd>              <span>to lock</span>            </span>          )}        </div>      )}    </div>  )}/* -------------------------------------------------------------------------- *//*  Main ForecastLine Component                                               *//* -------------------------------------------------------------------------- */export function ForecastLine<  TData extends Record<string, unknown> = Record<string, unknown>,  XVal extends string | number = string | number>({  data,  xKey,  series,  height = 320,  curve = "monotone",  domain = "auto",  actualColor = "var(--chart-1, #3b82f6)",  forecastColor = "var(--chart-2, #10b981)",  confidenceColor = "var(--chart-2, #10b981)",  confidenceOpacity = 0.18,  selectionColor = "var(--chart-selection, #f59e0b)",  showGrid = true,  showXAxis = true,  showYAxis = true,  showLegend = true,  interactiveLegend = true,  lockableTooltip = true,  defaultLockedIndex = null,  missingValuePolicy = "gap",  animation = "draw",  motion = true,  valueFormatter,  xFormatter,  onActiveDatumChange,  title,  description,  className,  loading = false,  error = null,  unavailable = false,}: ForecastLineProps<TData, XVal>) {  const reducedMotion = useChartReducedMotion()  const uid = React.useId()  const titleId = `plotcn-forecast-title-${uid}`  const descId = `plotcn-forecast-desc-${uid}`  const actualLabel = series?.actualLabel || "Actual"  const forecastLabel = series?.forecastLabel || "Forecast"  const confidenceLabel = series?.confidenceLabel || "Forecast range"  const hasConfidenceConfigured = Boolean(series?.lowerKey && series?.upperKey)  // 1. Normalize dataset  const normalizedData = React.useMemo(    () => normalizeForecastData(data, xKey, series, missingValuePolicy),    [data, xKey, series, missingValuePolicy]  )  // 2. Interactive Role Visibility State  const [rolesVisible, setRolesVisible] = React.useState({    actual: true,    forecast: true,    confidence: true,  })  const toggleRoleVisibility = (role: "actual" | "forecast" | "confidence") => {    if (!interactiveLegend) return    setRolesVisible((prev) => ({ ...prev, [role]: !prev[role] }))  }  const showAllRoles = () => {    setRolesVisible({ actual: true, forecast: true, confidence: true })  }  // Safe Y-axis domain  const safeDomain = React.useMemo(    () => calculateForecastDomain(normalizedData, domain, rolesVisible),    [normalizedData, domain, rolesVisible]  )  // 3. Inspection State: active hover vs locked persistent datum  const [lockedIndex, setLockedIndex] = React.useState<number | null>(() => {    if (      typeof defaultLockedIndex === "number" &&      defaultLockedIndex >= 0 &&      defaultLockedIndex < normalizedData.length    ) {      return defaultLockedIndex    }    return null  })  const [activeIndex, setActiveIndex] = React.useState<number | null>(() => {    if (      typeof defaultLockedIndex === "number" &&      defaultLockedIndex >= 0 &&      defaultLockedIndex < normalizedData.length    ) {      return defaultLockedIndex    }    return null  })  const [isChartFocused, setIsChartFocused] = React.useState(false)  // Clear locked index if data shrinks  React.useEffect(() => {    if (lockedIndex !== null && (lockedIndex >= normalizedData.length || normalizedData.length === 0)) {      setLockedIndex(null)    }  }, [normalizedData.length, lockedIndex])  const effectiveIndex = lockedIndex !== null ? lockedIndex : activeIndex  // Active datum payload  const activeDatum = React.useMemo<ForecastActiveDatum<TData, XVal> | null>(() => {    if (effectiveIndex === null || effectiveIndex < 0 || effectiveIndex >= normalizedData.length) {      return null    }    const row = normalizedData[effectiveIndex]    return {      index: effectiveIndex,      x: row.__x as XVal,      raw: row.__raw as TData,      actual: row.__actual,      forecast: row.__forecast,      lower: row.__lower,      upper: row.__upper,      isBridge: row.__isBridge,      isForecastPhase: row.__isForecastPhase,      isLocked: lockedIndex !== null && lockedIndex === effectiveIndex,    }  }, [effectiveIndex, lockedIndex, normalizedData])  React.useEffect(() => {    if (onActiveDatumChange) {      onActiveDatumChange(activeDatum)    }  }, [activeDatum, onActiveDatumChange])  /* -------------------------------------------------------------------------- */  /*  Keyboard & Pointer Interaction                                            */  /* -------------------------------------------------------------------------- */  const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {    if (normalizedData.length === 0) return    switch (e.key) {      case "ArrowRight": {        e.preventDefault()        const nextIdx = activeIndex === null ? 0 : Math.min(activeIndex + 1, normalizedData.length - 1)        setActiveIndex(nextIdx)        break      }      case "ArrowLeft": {        e.preventDefault()        const prevIdx =          activeIndex === null            ? normalizedData.length - 1            : Math.max(activeIndex - 1, 0)        setActiveIndex(prevIdx)        break      }      case "Home": {        e.preventDefault()        setActiveIndex(0)        break      }      case "End": {        e.preventDefault()        setActiveIndex(normalizedData.length - 1)        break      }      case "Enter":      case " ": {        if (!lockableTooltip) return        e.preventDefault()        const targetIndex = activeIndex !== null ? activeIndex : 0        if (lockedIndex === targetIndex) {          setLockedIndex(null)        } else {          setLockedIndex(targetIndex)          setActiveIndex(targetIndex)        }        break      }      case "Escape": {        e.preventDefault()        setLockedIndex(null)        setActiveIndex(null)        break      }    }  }  const handleChartMouseMove = (state: any) => {    if (!state || typeof state.activeTooltipIndex !== "number") return    const idx = state.activeTooltipIndex    if (idx < 0 || idx >= normalizedData.length) return    if (lockedIndex === null) {      if (activeIndex !== idx) {        setActiveIndex(idx)      }    }  }  const handleChartMouseLeave = () => {    if (lockedIndex === null) {      setActiveIndex(null)    }  }  const handleChartClick = (state: any) => {    if (!lockableTooltip || normalizedData.length === 0) return    const clickedIdx = typeof state?.activeTooltipIndex === "number" ? state.activeTooltipIndex : activeIndex    if (clickedIdx === null || clickedIdx < 0 || clickedIdx >= normalizedData.length) return    if (lockedIndex === clickedIdx) {      setLockedIndex(null)    } else {      setLockedIndex(clickedIdx)      setActiveIndex(clickedIdx)    }  }  // Animation settings  const isAnimated = motion !== false && !reducedMotion && animation !== "none"  const animationDuration =    typeof motion === "object" && motion?.duration !== undefined ? motion.duration * 1000 : 350  // Factual screen reader summary  const factualSummary = React.useMemo(() => {    if (normalizedData.length === 0) return "No forecast observations recorded."    const totalObs = normalizedData.length    const actualCount = normalizedData.filter((d) => d.__actual !== null).length    const forecastCount = normalizedData.filter((d) => d.__forecast !== null).length    const activeInfo = activeDatum      ? ` Inspected ${activeDatum.x}: ${activeDatum.actual !== null ? `actual ${activeDatum.actual}` : ""}${activeDatum.forecast !== null ? ` forecast ${activeDatum.forecast}` : ""}.`      : ""    return `Forecast line chart with ${actualCount} historical observations and ${forecastCount} forecast periods across ${totalObs} total time points.${activeInfo}`  }, [normalizedData, activeDatum])  /* -------------------------------------------------------------------------- */  /*  Early Return Error / Loading States                                       */  /* -------------------------------------------------------------------------- */  if (error) {    return (      <figure        role="region"        aria-label={title || "Forecast chart error state"}        className={cn("plotcn-forecast 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="Unable to render forecast signal"          description={typeof error === "string" ? error : error?.message || "An unexpected error occurred while loading forecast data."}        />      </figure>    )  }  if (loading) {    return (      <figure        role="region"        aria-label={title || "Forecast chart loading state"}        className={cn("plotcn-forecast 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          title="Loading forecast trajectory…"          description="Preparing historical observations and prediction interval"        />      </figure>    )  }  if (unavailable) {    return (      <figure        role="region"        aria-label={title || "Forecast chart unavailable state"}        className={cn("plotcn-forecast 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="Forecast metrics unavailable"          description={typeof unavailable === "string" ? unavailable : "Forecast trajectory data is currently unavailable."}        />      </figure>    )  }  if (normalizedData.length === 0) {    return (      <figure        role="region"        aria-label={title || "Forecast chart empty state"}        className={cn("plotcn-forecast 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 data available"          description="Provide ordered observations with actual and forecast metrics to display trajectory."        />      </figure>    )  }  const activeXCoordinate = effectiveIndex !== null ? normalizedData[effectiveIndex].__x : null  const isCurrentlyLocked = lockedIndex !== null  const allRolesHidden = !rolesVisible.actual && !rolesVisible.forecast && !rolesVisible.confidence  return (    <figure      role="region"      aria-labelledby={titleId}      aria-describedby={descId}      tabIndex={0}      onKeyDown={handleKeyDown}      onFocus={() => setIsChartFocused(true)}      onBlur={() => setIsChartFocused(false)}      className={cn(        "plotcn-forecast relative flex flex-col w-full outline-none select-none transition-all duration-150 rounded-xl",        "focus-visible:ring-2 focus-visible:ring-[var(--chart-focus,#38bdf8)] focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950",        className      )}      style={{        height,        minHeight: typeof height === "number" ? height : 320,        touchAction: "pan-y",      }}    >      {/* Screen Reader Announcement */}      <div className="sr-only">        <h3 id={titleId}>{title || "Observed History vs. Forecast Trajectory Chart"}</h3>        <p id={descId}>{description ? `${description} ${factualSummary}` : factualSummary}</p>      </div>      {/* Chart Canvas Area */}      <ChartContainer className="w-full flex-1 min-w-0 min-h-0 max-w-full overflow-hidden relative">        {allRolesHidden ? (          <div className="flex flex-col items-center justify-center h-full w-full py-12 px-4 text-center space-y-3 z-20">            <div className="size-10 rounded-full bg-zinc-900/80 border border-white/10 flex items-center justify-center text-zinc-400">              <HugeiconsIcon icon={ViewOffSlashIcon} size={20} />            </div>            <div className="space-y-1">              <p className="text-sm font-semibold text-zinc-200">All forecast elements hidden</p>              <p className="text-xs text-zinc-400 max-w-xs">                Use the legend below to show observed history or forecast trajectory.              </p>            </div>            {interactiveLegend && (              <button                type="button"                onClick={showAllRoles}                className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-white/[0.08] hover:bg-white/[0.14] border border-white/[0.12] text-xs font-mono font-medium text-white transition-all cursor-pointer"              >                <HugeiconsIcon icon={ViewIcon} size={14} />                <span>Show all</span>              </button>            )}          </div>        ) : (          <ResponsiveContainer            width="100%"            height="100%"            minWidth={0}            minHeight={0}            initialDimension={{ width: 320, height: typeof height === "number" ? height : 320 }}          >            <ComposedChart              data={normalizedData}              margin={{ top: 14, right: 16, bottom: 8, left: 8 }}              onMouseMove={handleChartMouseMove}              onMouseLeave={handleChartMouseLeave}              onClick={handleChartClick}            >              {/* Reference Grid */}              {showGrid && (                <CartesianGrid                  stroke="var(--chart-grid, rgba(255, 255, 255, 0.08))"                  strokeDasharray="3 3"                  vertical={false}                />              )}              {/* Horizontal Category Scale */}              <XAxis                hide={!showXAxis}                dataKey="__x"                tickLine={false}                axisLine={false}                tick={{ fontSize: 11, fill: "var(--chart-axis, #71717a)" }}                tickFormatter={xFormatter as any}                dy={6}              />              {/* Vertical Numeric Scale */}              <YAxis                hide={!showYAxis}                domain={safeDomain as any}                tickLine={false}                axisLine={false}                tick={{ fontSize: 11, fill: "var(--chart-axis, #71717a)" }}                tickFormatter={valueFormatter ? (v) => valueFormatter(Number(v)) : undefined}                dx={-4}              />              {/* Persistent Vertical Inspection Crosshair */}              {activeXCoordinate !== null && isCurrentlyLocked && (                <ReferenceLine                  x={activeXCoordinate}                  stroke={selectionColor}                  strokeDasharray="3 3"                  strokeWidth={1.5}                />              )}              {/* Synchronized Shared Tooltip */}              <Tooltip                active={isCurrentlyLocked || (isChartFocused && activeIndex !== null) ? true : undefined}                defaultIndex={typeof defaultLockedIndex === "number" ? defaultLockedIndex : undefined}                content={                  <ForecastTooltipContent                    activeX={activeXCoordinate}                    activeDatum={activeDatum}                    actualLabel={actualLabel}                    forecastLabel={forecastLabel}                    confidenceLabel={confidenceLabel}                    actualColor={actualColor}                    forecastColor={forecastColor}                    confidenceColor={confidenceColor}                    rolesVisible={rolesVisible}                    valueFormatter={valueFormatter}                    xFormatter={xFormatter}                    lockableTooltip={lockableTooltip}                    isLocked={isCurrentlyLocked}                    selectionColor={selectionColor}                  />                }                cursor={{                  stroke: isCurrentlyLocked ? selectionColor : "var(--chart-crosshair, rgba(255, 255, 255, 0.28))",                  strokeDasharray: "3 3",                  strokeWidth: isCurrentlyLocked ? 1.5 : 1.2,                }}              />              {/* 1. Confidence Range Area: Rendered behind trend lines */}              {rolesVisible.confidence && hasConfidenceConfigured && (                <Area                  type={curve === "step" ? "stepAfter" : curve}                  dataKey="__range"                  name={confidenceLabel}                  fill={confidenceColor}                  fillOpacity={confidenceOpacity}                  stroke="none"                  isAnimationActive={isAnimated}                  animationDuration={animationDuration}                  activeDot={false}                  dot={false}                  connectNulls={false}                />              )}              {/* 2. Observed History Line: Solid line */}              {rolesVisible.actual && (                <Line                  type={curve === "step" ? "stepAfter" : curve}                  dataKey="__actual"                  name={actualLabel}                  stroke={actualColor}                  strokeWidth={2}                  connectNulls={false}                  isAnimationActive={isAnimated}                  animationDuration={animationDuration}                  dot={(dotProps: any) => {                    const { cx, cy, index, payload } = dotProps                    if (typeof cx !== "number" || typeof cy !== "number" || payload.__actual === null) {                      return <React.Fragment key={`act-dot-${index}`} />                    }                    if (index === effectiveIndex) {                      if (payload.__isBridge) {                        return (                          <g key={`bridge-dot-${index}`} className="pointer-events-none">                            <circle cx={cx} cy={cy} r={7} fill="none" stroke={forecastColor} strokeWidth={2} strokeDasharray="2 2" />                            <circle cx={cx} cy={cy} r={4} fill={actualColor} stroke="var(--chart-background, #09090b)" strokeWidth={1.5} />                          </g>                        )                      }                      if (index === lockedIndex) {                        return (                          <g key={`locked-act-${index}`} className="pointer-events-none">                            <circle cx={cx} cy={cy} r={7} fill="none" stroke={actualColor} strokeWidth={2} />                            <circle cx={cx} cy={cy} r={3.5} fill={actualColor} stroke="var(--chart-background, #09090b)" strokeWidth={1.5} />                          </g>                        )                      }                      return (                        <circle                          key={`act-dot-${index}`}                          cx={cx}                          cy={cy}                          r={5}                          fill={actualColor}                          stroke="var(--chart-background, #09090b)"                          strokeWidth={2}                          className="pointer-events-none"                        />                      )                    }                    return <React.Fragment key={`act-dot-${index}`} />                  }}                  activeDot={(activeProps: any) => {                    const { cx, cy, index, payload } = activeProps                    if (typeof cx !== "number" || typeof cy !== "number" || payload?.__actual === null) {                      return <React.Fragment key={`act-hover-${index}`} />                    }                    return (                      <circle                        key={`act-hover-${index}`}                        cx={cx}                        cy={cy}                        r={5.5}                        fill={actualColor}                        stroke="var(--chart-background, #09090b)"                        strokeWidth={2}                        className="pointer-events-none"                      />                    )                  }}                />              )}              {/* 3. Forecast Prediction Line: Dashed line */}              {rolesVisible.forecast && (                <Line                  type={curve === "step" ? "stepAfter" : curve}                  dataKey="__forecast"                  name={forecastLabel}                  stroke={forecastColor}                  strokeWidth={2}                  strokeDasharray="5 5"                  connectNulls={false}                  isAnimationActive={isAnimated}                  animationDuration={animationDuration}                  dot={(dotProps: any) => {                    const { cx, cy, index, payload } = dotProps                    if (typeof cx !== "number" || typeof cy !== "number" || payload.__forecast === null) {                      return <React.Fragment key={`fc-dot-${index}`} />                    }                    // If it's a bridge point and actual is also rendering, let actual handle the composite marker                    if (payload.__isBridge && rolesVisible.actual) {                      return <React.Fragment key={`fc-bridge-skip-${index}`} />                    }                    if (index === effectiveIndex) {                      if (index === lockedIndex) {                        return (                          <g key={`locked-fc-${index}`} className="pointer-events-none">                            <circle cx={cx} cy={cy} r={7} fill="none" stroke={forecastColor} strokeWidth={2} />                            <circle cx={cx} cy={cy} r={3.5} fill={forecastColor} stroke="var(--chart-background, #09090b)" strokeWidth={1.5} />                          </g>                        )                      }                      return (                        <circle                          key={`fc-dot-${index}`}                          cx={cx}                          cy={cy}                          r={5}                          fill={forecastColor}                          stroke="var(--chart-background, #09090b)"                          strokeWidth={2}                          className="pointer-events-none"                        />                      )                    }                    return <React.Fragment key={`fc-dot-${index}`} />                  }}                  activeDot={(activeProps: any) => {                    const { cx, cy, index, payload } = activeProps                    if (typeof cx !== "number" || typeof cy !== "number" || payload?.__forecast === null) {                      return <React.Fragment key={`fc-hover-${index}`} />                    }                    return (                      <circle                        key={`fc-hover-${index}`}                        cx={cx}                        cy={cy}                        r={5.5}                        fill={forecastColor}                        stroke="var(--chart-background, #09090b)"                        strokeWidth={2}                        className="pointer-events-none"                      />                    )                  }}                />              )}            </ComposedChart>          </ResponsiveContainer>        )}      </ChartContainer>      {/* Interactive Responsive Legend */}      {showLegend && (        <div          role="group"          aria-label="Forecast series visibility controls"          className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 pt-3 px-2 text-xs font-mono select-none"        >          {/* Actual item */}          <button            type="button"            role="switch"            aria-checked={rolesVisible.actual}            aria-label={`${actualLabel}: ${rolesVisible.actual ? "visible, click to hide" : "hidden, click to show"}`}            disabled={!interactiveLegend}            onClick={() => toggleRoleVisibility("actual")}            className={cn(              "inline-flex items-center gap-2 px-2.5 py-1 rounded-md transition-all",              interactiveLegend                ? "cursor-pointer hover:bg-white/[0.06] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/30"                : "cursor-default",              !rolesVisible.actual ? "opacity-35 line-through text-zinc-500" : "opacity-100 text-zinc-300 font-medium"            )}          >            <svg width={16} height={6} className="shrink-0 overflow-visible" aria-hidden="true">              <line x1={0} y1={3} x2={16} y2={3} stroke={rolesVisible.actual ? actualColor : "currentColor"} strokeWidth={2.5} />            </svg>            <span>{actualLabel}</span>          </button>          {/* Forecast item */}          <button            type="button"            role="switch"            aria-checked={rolesVisible.forecast}            aria-label={`${forecastLabel}: ${rolesVisible.forecast ? "visible, click to hide" : "hidden, click to show"}`}            disabled={!interactiveLegend}            onClick={() => toggleRoleVisibility("forecast")}            className={cn(              "inline-flex items-center gap-2 px-2.5 py-1 rounded-md transition-all",              interactiveLegend                ? "cursor-pointer hover:bg-white/[0.06] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/30"                : "cursor-default",              !rolesVisible.forecast ? "opacity-35 line-through text-zinc-500" : "opacity-100 text-zinc-300 font-medium"            )}          >            <svg width={16} height={6} className="shrink-0 overflow-visible" aria-hidden="true">              <line x1={0} y1={3} x2={16} y2={3} stroke={rolesVisible.forecast ? forecastColor : "currentColor"} strokeWidth={2.5} strokeDasharray="4 3" />            </svg>            <span>{forecastLabel}</span>          </button>          {/* Confidence interval item */}          {hasConfidenceConfigured && (            <button              type="button"              role="switch"              aria-checked={rolesVisible.confidence}              aria-label={`${confidenceLabel}: ${rolesVisible.confidence ? "visible, click to hide" : "hidden, click to show"}`}              disabled={!interactiveLegend}              onClick={() => toggleRoleVisibility("confidence")}              className={cn(                "inline-flex items-center gap-2 px-2.5 py-1 rounded-md transition-all",                interactiveLegend                  ? "cursor-pointer hover:bg-white/[0.06] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/30"                  : "cursor-default",                !rolesVisible.confidence ? "opacity-35 line-through text-zinc-500" : "opacity-100 text-zinc-300 font-medium"              )}            >              <span                className="size-3 rounded-xs shrink-0 border"                style={{                  backgroundColor: rolesVisible.confidence ? `${confidenceColor}33` : "transparent",                  borderColor: rolesVisible.confidence ? confidenceColor : "currentColor",                }}              />              <span>{confidenceLabel}</span>            </button>          )}        </div>      )}    </figure>  )}