shadimage
plotcn
componentarea-baselinesource: ok · hit

Baseline Area

Single-series area visualization showing values above and below an explicit quantitative reference baseline with truthful deviation semantics.

npx shadcn add "https://plotcn.vercel.app/r/area-baseline.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
chartsrecharts

Registry dependencies

https://plotcn.vercel.app/r/chart-container.jsonhttps://plotcn.vercel.app/r/chart-state.jsonhttps://plotcn.vercel.app/r/chart-tooltip.jsonhttps://plotcn.vercel.app/r/chart-motion.json

Files1 · source cached 10/8/2026, 2:10:06 PM

components/charts/recharts/area-baseline.tsx
"use client"

import * as React from "react"
import {
  ResponsiveContainer,
  AreaChart,
  Area,
  XAxis,
  YAxis,
  Tooltip,
  CartesianGrid,
  ReferenceLine,
} from "recharts"
import { HugeiconsIcon } from "@hugeicons/react"
import { LockKeyIcon } from "@hugeicons/core-free-icons"
import { useChartReducedMotion } from "../shared/use-chart-reduced-motion"
import { 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

export type BaselineClassification = "above" | "equal" | "below"

/**
 * Series definition for Baseline Area (single quantitative series relative to explicit baseline).
 */
export interface BaselineAreaSeries<TData extends Record<string, unknown> = Record<string, unknown>> {
  /** Property key for quantitative observation values */
  key: NumericKeyOf<TData>

  /** Human-readable display label for legend, tooltips, and screen readers */
  label: string

  /** Optional custom numeric formatter for metric values */
  valueFormatter?: (value: number) => string
}

export interface BaselineAreaActiveDatum<
  TData extends Record<string, unknown> = Record<string, unknown>,
  XVal extends string | number = string | number
> {
  index: number
  x: XVal
  raw: TData
  value: number | null
  baseline: number
  deviation: number | null
  position: BaselineClassification | "unavailable"
  isLocked: boolean
}

export interface BaselineAreaProps<
  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[]

  /** Key for horizontal domain coordinate (e.g. date, month, hour). */
  xKey: keyof TData & string

  /** Semantic series descriptor defining metric key and label. */
  series: BaselineAreaSeries<TData>

  /**
   * Explicit constant reference baseline value.
   * REQUIRED: Plotcn never silently defaults to zero.
   */
  baseline: number

  /** Optional label for the reference baseline (e.g. "Target", "SLA", "Benchmark"). */
  baselineLabel?: string

  /** Optional label for values exceeding the baseline. (default: "Above reference") */
  aboveLabel?: string

  /** Optional label for values below the baseline. (default: "Below reference") */
  belowLabel?: string

  /** Container height in pixels or CSS dimension string. (default: 320) */
  height?: number | string

  /** Curve interpolation: "monotone" | "linear" | "step". (default: "monotone") */
  curve?: "monotone" | "linear" | "step"

  /** Explicit Y-axis numeric domain, or "auto" calculation. */
  domain?: [number, number] | "auto"

  /** Primary signal stroke and active point color. (default: "var(--chart-1, #3b82f6)") */
  color?: string

  /** Fill color for occupied area above the baseline. (default: "var(--chart-1, #3b82f6)") */
  aboveColor?: string

  /** Fill color for occupied area below the baseline. (default: "var(--chart-2, #10b981)") */
  belowColor?: string

  /** Reference baseline stroke and indicator color. (default: "var(--chart-axis, #71717a)") */
  baselineColor?: string

  /** Active selection / crosshair highlight color. (default: "var(--chart-selection, #38bdf8)") */
  selectionColor?: string

  /** Fill opacity for above and below occupied regions. (default: 0.24) */
  fillOpacity?: number

  /** Whether to render subtle horizontal grid reference lines. (default: true) */
  showGrid?: boolean

  /** Whether to render horizontal domain tick labels. (default: true) */
  showXAxis?: boolean

  /** Whether to render vertical metric scale ticks. (default: true) */
  showYAxis?: boolean

  /** Whether to render above/below/baseline status legend. (default: false) */
  showLegend?: boolean

  /** Whether to derive and display signed deviation in the inspection tooltip. (default: true) */
  showDeviation?: boolean

  /** Whether to allow locking tooltip inspection on click or Enter/Space. (default: true) */
  lockableTooltip?: boolean

  /** Handling of missing/null values: "gap" (truthful break) | "connect" (bridge). (default: "gap") */
  missingValuePolicy?: "gap" | "connect"

  /** Motion preferences. (default: true) */
  motion?: boolean | { duration?: number }

  /** Initial inspection lock index. */
  defaultLockedIndex?: number | null

  /** Callback fired when the active inspection datum changes. */
  onActiveChange?: (active: BaselineAreaActiveDatum<TData, XVal> | null) => void

  /** Optional custom title announced to assistive technology. */
  title?: string

  /** Optional extended description for assistive technology. */
  description?: string

  /** Explicit loading state fallback. */
  loading?: boolean

  /** Explicit empty state fallback. */
  empty?: boolean

  /** Explicit error state fallback. */
  error?: string | Error

  /** Explicit unavailable state fallback. */
  unavailable?: boolean

  /** Additional CSS class for outer container figure. */
  className?: string
}

/* -------------------------------------------------------------------------- */
/*  Pure Mathematical Helpers                                                 */
/* -------------------------------------------------------------------------- */

/**
 * Factual classification of an observed numeric value relative to an explicit baseline.
 * Does not assign judgmental "good/bad" value labels.
 */
export function classifyAgainstBaseline(
  value: number,
  baseline: number
): BaselineClassification {
  if (value > baseline) return "above"
  if (value < baseline) return "below"
  return "equal"
}

/**
 * Normalizes input records into clean numeric data.
 * Clamps or sanitizes non-finite values to null to protect SVG geometry.
 */
export function normalizeBaselineData<TData extends Record<string, unknown>>(
  data: readonly TData[],
  key: string
): Array<TData & { _normalizedValue: number | null }> {
  if (!Array.isArray(data)) return []

  return data.map((item) => {
    if (!item || typeof item !== "object") {
      return { ...item, _normalizedValue: null }
    }
    const val = item[key]
    const num = typeof val === "number" && Number.isFinite(val) ? val : null
    return {
      ...item,
      _normalizedValue: num,
    }
  })
}

/**
 * Computes a safe Y-domain enclosing all valid series observations AND the explicit baseline.
 */
export function calculateBaselineAreaDomain<TData extends Record<string, unknown>>(
  data: ReadonlyArray<TData & { _normalizedValue: number | null }>,
  baseline: number,
  explicitDomain?: [number, number] | "auto"
): [number, number] {
  if (
    Array.isArray(explicitDomain) &&
    explicitDomain.length === 2 &&
    typeof explicitDomain[0] === "number" &&
    typeof explicitDomain[1] === "number" &&
    Number.isFinite(explicitDomain[0]) &&
    Number.isFinite(explicitDomain[1])
  ) {
    return [explicitDomain[0], explicitDomain[1]]
  }

  const validValues = data
    .map((d) => d._normalizedValue)
    .filter((v): v is number => typeof v === "number" && Number.isFinite(v))

  if (validValues.length === 0) {
    if (Number.isFinite(baseline)) {
      return [baseline - 10, baseline + 10]
    }
    return [0, 100]
  }

  const rawMin = Math.min(...validValues, baseline)
  const rawMax = Math.max(...validValues, baseline)

  if (rawMin === rawMax) {
    const pad = Math.abs(rawMin) > 0 ? Math.abs(rawMin) * 0.15 : 10
    return [Number((rawMin - pad).toFixed(2)), Number((rawMax + pad).toFixed(2))]
  }

  const span = rawMax - rawMin
  const padding = span * 0.08

  return [
    Number((rawMin - padding).toFixed(2)),
    Number((rawMax + padding).toFixed(2)),
  ]
}

/**
 * Computes normalized vertical offset (0.0 to 1.0) of the baseline within the Area polygon's bounding box.
 * The polygon's top is Math.max(baseline, ...values) (y=0 in objectBoundingBox),
 * and its bottom is Math.min(baseline, ...values) (y=1 in objectBoundingBox).
 */
export function calculateBaselineGradientOffset(
  min: number,
  max: number,
  baseline: number
): number {
  if (!Number.isFinite(min) || !Number.isFinite(max) || !Number.isFinite(baseline)) {
    return 0.5
  }
  if (max === min) {
    return 0.5
  }
  const offset = (max - baseline) / (max - min)
  return Math.max(0, Math.min(1, offset))
}

/* -------------------------------------------------------------------------- */
/*  Main Component: BaselineArea                                              */
/* -------------------------------------------------------------------------- */

export function BaselineArea<
  TData extends Record<string, unknown> = Record<string, unknown>,
  XVal extends string | number = string | number
>({
  data = [],
  xKey,
  series,
  baseline,
  baselineLabel = "Reference",
  aboveLabel = "Above reference",
  belowLabel = "Below reference",
  height = 320,
  curve = "monotone",
  domain = "auto",
  color = "var(--chart-1, #3b82f6)",
  aboveColor = "var(--chart-1, #3b82f6)",
  belowColor = "var(--chart-2, #10b981)",
  baselineColor = "var(--chart-axis, #71717a)",
  selectionColor = "var(--chart-selection, #38bdf8)",
  fillOpacity = 0.24,
  showGrid = true,
  showXAxis = true,
  showYAxis = true,
  showLegend = false,
  showDeviation = true,
  lockableTooltip = true,
  missingValuePolicy = "gap",
  motion = true,
  defaultLockedIndex = null,
  onActiveChange,
  title = "Baseline Area Chart",
  description,
  loading = false,
  empty = false,
  error,
  unavailable = false,
  className,
}: BaselineAreaProps<TData, XVal>) {
  // 1. Validate explicit baseline requirement
  const isBaselineValid = typeof baseline === "number" && Number.isFinite(baseline)

  // 2. Reduced motion detection
  const prefersReducedMotion = useChartReducedMotion()
  const isMotionEnabled = motion !== false && !prefersReducedMotion

  // 3. Normalize dataset
  const normalizedData = React.useMemo(() => {
    return normalizeBaselineData(data, series.key)
  }, [data, series.key])

  // 4. Safe Y domain enclosing both data and baseline
  const safeDomain = React.useMemo(() => {
    if (!isBaselineValid) return [0, 100] as [number, number]
    return calculateBaselineAreaDomain(normalizedData, baseline, domain)
  }, [normalizedData, baseline, domain, isBaselineValid])

  // 5. Data range for polygon bounding box
  const polygonBounds = React.useMemo(() => {
    if (!isBaselineValid) return { min: 0, max: 100 }
    const validValues = normalizedData
      .map((d) => d._normalizedValue)
      .filter((v): v is number => typeof v === "number" && Number.isFinite(v))

    if (validValues.length === 0) {
      return { min: baseline, max: baseline }
    }
    return {
      min: Math.min(...validValues, baseline),
      max: Math.max(...validValues, baseline),
    }
  }, [normalizedData, baseline, isBaselineValid])

  // 6. Hard-stop gradient offset calculation
  const baselineOffset = React.useMemo(() => {
    if (!isBaselineValid) return 0.5
    return calculateBaselineGradientOffset(polygonBounds.min, polygonBounds.max, baseline)
  }, [polygonBounds.min, polygonBounds.max, baseline, isBaselineValid])

  const offsetPercent = React.useMemo(() => {
    return Number((baselineOffset * 100).toFixed(3))
  }, [baselineOffset])

  // 7. SSR-safe deterministic gradient and clip IDs
  const rawId = React.useId()
  const gradientId = React.useMemo(
    () => `plotcn-baseline-${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`,
    [rawId]
  )

  // 8. Opacity clamping
  const clampedOpacity = Math.max(
    0,
    Math.min(1, typeof fillOpacity === "number" && Number.isFinite(fillOpacity) ? fillOpacity : 0.24)
  )

  // 9. Inspection state
  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 (lockedIndex !== null) return lockedIndex
    return normalizedData.length > 0 ? 0 : null
  })

  // Synchronize inspection datum callback
  React.useEffect(() => {
    if (!onActiveChange) return
    const idx = lockedIndex !== null ? lockedIndex : activeIndex
    if (idx === null || idx < 0 || idx >= normalizedData.length) {
      onActiveChange(null)
      return
    }

    const item = normalizedData[idx]
    const val = item._normalizedValue
    const dev = val !== null && isBaselineValid ? val - baseline : null
    const pos: BaselineClassification | "unavailable" =
      val === null || !isBaselineValid
        ? "unavailable"
        : classifyAgainstBaseline(val, baseline)

    onActiveChange({
      index: idx,
      x: item[xKey] as XVal,
      raw: item,
      value: val,
      baseline,
      deviation: dev,
      position: pos,
      isLocked: lockedIndex !== null,
    })
  }, [activeIndex, lockedIndex, normalizedData, xKey, baseline, isBaselineValid, onActiveChange])

  // Keyboard navigation on root figure
  const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
    if (normalizedData.length === 0) return

    const currentIndex = activeIndex ?? 0

    switch (e.key) {
      case "ArrowLeft": {
        e.preventDefault()
        const next = Math.max(0, currentIndex - 1)
        setActiveIndex(next)
        if (lockedIndex !== null) setLockedIndex(next)
        break
      }
      case "ArrowRight": {
        e.preventDefault()
        const next = Math.min(normalizedData.length - 1, currentIndex + 1)
        setActiveIndex(next)
        if (lockedIndex !== null) setLockedIndex(next)
        break
      }
      case "Home": {
        e.preventDefault()
        setActiveIndex(0)
        if (lockedIndex !== null) setLockedIndex(0)
        break
      }
      case "End": {
        e.preventDefault()
        const last = normalizedData.length - 1
        setActiveIndex(last)
        if (lockedIndex !== null) setLockedIndex(last)
        break
      }
      case "Enter":
      case " ": {
        if (!lockableTooltip) return
        e.preventDefault()
        if (lockedIndex === activeIndex) {
          setLockedIndex(null)
        } else {
          setLockedIndex(activeIndex)
        }
        break
      }
      case "Escape": {
        e.preventDefault()
        setLockedIndex(null)
        break
      }
    }
  }

  // Fallback states
  if (loading) {
    return (
      <ChartContainer style={{ height }} className={className}>
        <ChartLoadingState description="Loading baseline observation records..." />
      </ChartContainer>
    )
  }

  if (unavailable) {
    return (
      <ChartContainer style={{ height }} className={className}>
        <ChartUnavailableState description="Baseline Area data service is temporarily unavailable." />
      </ChartContainer>
    )
  }

  if (!isBaselineValid) {
    return (
      <ChartContainer style={{ height }} className={className}>
        <ChartErrorState
          title="Invalid baseline reference"
          description="baseline must be an explicit finite number."
        />
      </ChartContainer>
    )
  }

  if (error) {
    const errorDescription =
      error instanceof Error ? error.message : typeof error === "string" ? error : "An error occurred."
    return (
      <ChartContainer style={{ height }} className={className}>
        <ChartErrorState title="Baseline Area Error" description={errorDescription} />
      </ChartContainer>
    )
  }

  if (empty || normalizedData.length === 0) {
    return (
      <ChartContainer style={{ height }} className={className}>
        <ChartEmptyState description="No observations available for Baseline Area." />
      </ChartContainer>
    )
  }

  // Active observation computation
  const activeObsIndex = lockedIndex !== null ? lockedIndex : activeIndex
  const activeRecord =
    activeObsIndex !== null && activeObsIndex >= 0 && activeObsIndex < normalizedData.length
      ? normalizedData[activeObsIndex]
      : null

  const activeRawValue = activeRecord ? activeRecord._normalizedValue : null
  const activeDeviation =
    activeRawValue !== null ? activeRawValue - baseline : null
  const activeClassification =
    activeRawValue !== null
      ? classifyAgainstBaseline(activeRawValue, baseline)
      : "unavailable"

  return (
    <figure
      role="region"
      aria-label={title}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      className={cn(
        "relative flex flex-col w-full focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--chart-focus,#3b82f6)] focus-visible:ring-offset-2 rounded-xl",
        className
      )}
    >
      {/* Optional Accessible Screen Reader Caption */}
      <figcaption className="sr-only">
        {title}. Reference baseline is set to {baseline}.{" "}
        {description ||
          `Single quantitative series comparing ${series.label} above and below the reference across ${normalizedData.length} observations.`}
      </figcaption>

      {/* Main Recharts Area */}
      <ChartContainer style={{ height }}>
        <ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
          <AreaChart
            data={normalizedData as any}
            margin={{ top: 16, right: 16, left: 8, bottom: 8 }}
            onMouseMove={(state) => {
              if (lockedIndex !== null) return
              if (state && state.activeTooltipIndex !== undefined) {
                setActiveIndex(Number(state.activeTooltipIndex))
              }
            }}
            onMouseLeave={() => {
              if (lockedIndex !== null) return
              setActiveIndex(null)
            }}
            onClick={(state) => {
              if (!lockableTooltip) return
              if (state && state.activeTooltipIndex !== undefined) {
                const clicked = Number(state.activeTooltipIndex)
                setLockedIndex(lockedIndex === clicked ? null : clicked)
                setActiveIndex(clicked)
              }
            }}
          >
            <defs>
              <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
                {offsetPercent <= 0 ? (
                  <>
                    <stop offset="0%" stopColor={belowColor} stopOpacity={clampedOpacity} />
                    <stop offset="100%" stopColor={belowColor} stopOpacity={clampedOpacity} />
                  </>
                ) : offsetPercent >= 100 ? (
                  <>
                    <stop offset="0%" stopColor={aboveColor} stopOpacity={clampedOpacity} />
                    <stop offset="100%" stopColor={aboveColor} stopOpacity={clampedOpacity} />
                  </>
                ) : (
                  <>
                    <stop offset="0%" stopColor={aboveColor} stopOpacity={clampedOpacity} />
                    <stop offset={`${offsetPercent}%`} stopColor={aboveColor} stopOpacity={clampedOpacity} />
                    <stop offset={`${offsetPercent}%`} stopColor={belowColor} stopOpacity={clampedOpacity} />
                    <stop offset="100%" stopColor={belowColor} stopOpacity={clampedOpacity} />
                  </>
                )}
              </linearGradient>
            </defs>

            {showGrid && (
              <CartesianGrid
                strokeDasharray="3 3"
                stroke="var(--border, rgba(255, 255, 255, 0.08))"
                vertical={false}
              />
            )}

            {showXAxis && (
              <XAxis
                dataKey={xKey as any}
                axisLine={false}
                tickLine={false}
                tick={{ fill: "var(--muted-foreground, #a1a1aa)", fontSize: 11 }}
                dy={6}
              />
            )}

            {showYAxis && (
              <YAxis
                domain={safeDomain}
                axisLine={false}
                tickLine={false}
                tick={{ fill: "var(--muted-foreground, #a1a1aa)", fontSize: 11 }}
                dx={-4}
                tickFormatter={(v) =>
                  series.valueFormatter ? series.valueFormatter(v) : v.toLocaleString()
                }
              />
            )}

            {/* Explicit Baseline Reference Line */}
            <ReferenceLine
              y={baseline}
              stroke={baselineColor}
              strokeDasharray="4 4"
              strokeWidth={1.5}
              label={
                baselineLabel
                  ? {
                      value: `${baselineLabel} (${baseline})`,
                      position: "insideTopRight",
                      fill: "var(--muted-foreground, #a1a1aa)",
                      fontSize: 10,
                      fontFamily: "monospace",
                      dy: -8,
                    }
                  : undefined
              }
            />

            {/* Persistent Locked Crosshair */}
            {lockedIndex !== null && activeRecord && (
              <ReferenceLine
                x={activeRecord[xKey] as any}
                stroke={selectionColor}
                strokeWidth={1.5}
                strokeDasharray="2 2"
              />
            )}

            {/* Main Area Geometry with Baseline BaseValue & Hard-Stop Fill */}
            <Area
              type={curve}
              dataKey="_normalizedValue"
              baseValue={baseline}
              stroke={color}
              strokeWidth={2}
              fill={`url(#${gradientId})`}
              fillOpacity={1}
              isAnimationActive={isMotionEnabled}
              animationDuration={
                typeof motion === "object" && motion.duration ? motion.duration : 400
              }
              connectNulls={missingValuePolicy === "connect"}
              activeDot={{
                r: 5,
                fill: color,
                stroke: "var(--background, #09090b)",
                strokeWidth: 2,
              }}
            />

            {/* Synchronized Nearest-X Tooltip */}
            <Tooltip
              isAnimationActive={false}
              allowEscapeViewBox={{ x: false, y: false }}
              cursor={{
                stroke: lockedIndex !== null ? "transparent" : "var(--chart-crosshair, rgba(255,255,255,0.15))",
                strokeWidth: 1,
                strokeDasharray: "3 3",
              }}
              content={({ active, payload }) => {
                if (!active || !payload || payload.length === 0) return null
                const datum = payload[0].payload as TData & { _normalizedValue: number | null }
                const val = datum._normalizedValue
                const dev = val !== null ? val - baseline : null
                const classification =
                  val !== null ? classifyAgainstBaseline(val, baseline) : "unavailable"

                const formattedVal =
                  val !== null
                    ? series.valueFormatter
                      ? series.valueFormatter(val)
                      : val.toLocaleString()
                    : "—"

                const formattedBaseline = series.valueFormatter
                  ? series.valueFormatter(baseline)
                  : baseline.toLocaleString()

                const formattedDev =
                  dev !== null
                    ? series.valueFormatter
                      ? series.valueFormatter(dev)
                      : `${dev > 0 ? "+" : ""}${dev.toLocaleString()}`
                    : "—"

                const isLocked = lockedIndex !== null

                let positionText = "Unavailable"
                let positionBadgeColor = "bg-zinc-500/10 text-zinc-400 border-zinc-500/20"

                if (classification === "above") {
                  positionText = aboveLabel
                  positionBadgeColor = "bg-blue-500/10 text-blue-400 border-blue-500/25"
                } else if (classification === "below") {
                  positionText = belowLabel
                  positionBadgeColor = "bg-emerald-500/10 text-emerald-400 border-emerald-500/25"
                } else if (classification === "equal") {
                  positionText = "On reference"
                  positionBadgeColor = "bg-zinc-400/10 text-zinc-300 border-zinc-400/25"
                }

                return (
                  <div
                    role="tooltip"
                    className="plotcn-chart-tooltip rounded-lg border border-border/60 bg-popover/95 p-3 text-popover-foreground shadow-xl backdrop-blur-md min-w-[min(180px,calc(100cqw-16px))] max-w-[min(300px,calc(100cqw-16px))] max-h-[calc(100cqh-16px)] overflow-y-auto text-xs space-y-2"
                  >
                    <div className="flex items-center justify-between border-b border-border/40 pb-1.5 gap-2">
                      <span className="font-mono font-medium text-foreground text-xs">
                        {String(datum[xKey])}
                      </span>
                      {isLocked && (
                        <span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase text-sky-400 font-semibold">
                          <HugeiconsIcon icon={LockKeyIcon} size={11} /> Locked
                        </span>
                      )}
                    </div>

                    {/* Series Value */}
                    <div className="flex items-center justify-between gap-4">
                      <div className="flex items-center gap-1.5">
                        <span
                          className="size-2 rounded-full"
                          style={{ backgroundColor: color }}
                        />
                        <span className="text-muted-foreground">{series.label}</span>
                      </div>
                      <span className="font-mono font-semibold text-foreground">
                        {formattedVal}
                      </span>
                    </div>

                    {/* Baseline Reference */}
                    <div className="flex items-center justify-between gap-4">
                      <div className="flex items-center gap-1.5">
                        <span
                          className="w-2 h-0.5"
                          style={{ backgroundColor: baselineColor }}
                        />
                        <span className="text-muted-foreground">{baselineLabel}</span>
                      </div>
                      <span className="font-mono text-muted-foreground">
                        {formattedBaseline}
                      </span>
                    </div>

                    {/* Factual Signed Deviation */}
                    {showDeviation && dev !== null && (
                      <div className="flex items-center justify-between gap-4 pt-1 border-t border-border/30">
                        <span className="text-muted-foreground">Deviation</span>
                        <span
                          className={cn(
                            "font-mono font-bold",
                            dev > 0 ? "text-blue-400" : dev < 0 ? "text-emerald-400" : "text-muted-foreground"
                          )}
                        >
                          {dev > 0 ? `+${series.valueFormatter ? series.valueFormatter(dev) : dev.toLocaleString()}` : dev < 0 ? `-${series.valueFormatter ? series.valueFormatter(Math.abs(dev)) : Math.abs(dev).toLocaleString()}` : "0"}
                        </span>
                      </div>
                    )}

                    {/* Relative Position Classification */}
                    <div className="pt-0.5">
                      <span
                        className={cn(
                          "inline-block w-full text-center text-[10px] font-mono px-1.5 py-0.5 rounded border",
                          positionBadgeColor
                        )}
                      >
                        {positionText}
                      </span>
                    </div>
                  </div>
                )
              }}
            />
          </AreaChart>
        </ResponsiveContainer>
      </ChartContainer>

      {/* Optional Series / Baseline Status Legend */}
      {showLegend && (
        <div className="mt-3 flex flex-wrap items-center justify-center gap-4 text-xs font-mono text-muted-foreground">
          <div className="flex items-center gap-1.5">
            <span
              className="size-2.5 rounded-xs border"
              style={{
                backgroundColor: aboveColor,
                borderColor: aboveColor,
                opacity: clampedOpacity + 0.3,
              }}
            />
            <span>{aboveLabel}</span>
          </div>
          <div className="flex items-center gap-1.5">
            <span
              className="size-2.5 rounded-xs border"
              style={{
                backgroundColor: belowColor,
                borderColor: belowColor,
                opacity: clampedOpacity + 0.3,
              }}
            />
            <span>{belowLabel}</span>
          </div>
          <div className="flex items-center gap-1.5">
            <span
              className="w-3.5 h-0.5 border-t border-dashed"
              style={{ borderColor: baselineColor }}
            />
            <span>{baselineLabel} ({baseline})</span>
          </div>
        </div>
      )}

      {/* Off-screen Structured HTML Data Table for Screen Readers */}
      <div className="sr-only">
        <table>
          <caption>{title} - Data Table</caption>
          <thead>
            <tr>
              <th scope="col">{xKey}</th>
              <th scope="col">{series.label}</th>
              <th scope="col">Reference Baseline</th>
              <th scope="col">Deviation</th>
              <th scope="col">Position</th>
            </tr>
          </thead>
          <tbody>
            {normalizedData.map((row, idx) => {
              const val = row._normalizedValue
              const dev = val !== null ? val - baseline : null
              const pos = val !== null ? classifyAgainstBaseline(val, baseline) : "Unavailable"
              return (
                <tr key={idx}>
                  <td>{String(row[xKey])}</td>
                  <td>{val !== null ? val : "Unavailable"}</td>
                  <td>{baseline}</td>
                  <td>
                    {dev !== null
                      ? dev > 0
                        ? `+${dev}`
                        : `${dev}`
                      : "Unavailable"}
                  </td>
                  <td>{pos}</td>
                </tr>
              )
            })}
          </tbody>
        </table>
      </div>
    </figure>
  )
}

More from plotcn

Browse all components in this library →