'use client'; import { useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import Link from 'next/link'; import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { BarChart3, RefreshCcw, Search, X } from 'lucide-react'; import { AppShell } from '@/components/shell/app-shell'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Panel } from '@/components/ui/panel'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { useLinkPrefetch } from '@/hooks/use-link-prefetch'; import { GRAPH_CADENCE_OPTIONS, GRAPH_CHART_OPTIONS, GRAPH_SCALE_OPTIONS, GRAPH_SURFACE_LABELS, buildGraphingHref, getGraphMetricDefinition, metricsForSurfaceAndCadence, normalizeGraphTickers, parseGraphingParams, resolveGraphMetric, serializeGraphingParams, type GraphChartKind, type GraphMetricDefinition, type GraphingUrlState } from '@/lib/graphing/catalog'; import { buildGraphingComparisonData, type GraphingChartDatum, type GraphingFetchResult, type GraphingLatestValueRow, type GraphingSeriesPoint } from '@/lib/graphing/series'; import { ApiError } from '@/lib/api'; import { formatCurrencyByScale, formatPercent, type NumberScaleUnit } from '@/lib/format'; import type { FinancialCadence, FinancialUnit } from '@/lib/types'; import { companyFinancialStatementsQueryOptions } from '@/lib/query/options'; import { cn } from '@/lib/utils'; const CHART_COLORS = ['#68ffd5', '#5fd3ff', '#ffd08a', '#ff8a8a', '#c39bff'] as const; const CHART_MUTED = '#b4ced9'; const CHART_GRID = 'rgba(126, 217, 255, 0.24)'; const CHART_TOOLTIP_BG = 'rgba(6, 17, 24, 0.95)'; const CHART_TOOLTIP_BORDER = 'rgba(123, 255, 217, 0.45)'; type TooltipEntry = { dataKey?: string | number; color?: string; value?: number | string | null; payload?: GraphingChartDatum; }; function formatLongDate(value: string | null) { if (!value) { return 'n/a'; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return 'n/a'; } return format(parsed, 'MMM dd, yyyy'); } function formatShortDate(value: string) { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return value; } return format(parsed, 'MMM yyyy'); } function formatMetricValue( value: number | null | undefined, unit: FinancialUnit, scale: NumberScaleUnit ) { if (value === null || value === undefined) { return 'n/a'; } switch (unit) { case 'currency': return formatCurrencyByScale(value, scale); case 'percent': return formatPercent(value * 100); case 'ratio': return `${value.toFixed(2)}x`; case 'shares': case 'count': return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(value); default: return String(value); } } function formatChangeValue( value: number | null, unit: FinancialUnit, scale: NumberScaleUnit ) { if (value === null) { return 'n/a'; } if (unit === 'percent') { const signed = value >= 0 ? '+' : ''; return `${signed}${formatPercent(value * 100)}`; } if (unit === 'ratio') { const signed = value >= 0 ? '+' : ''; return `${signed}${value.toFixed(2)}x`; } if (unit === 'currency') { const formatted = formatCurrencyByScale(Math.abs(value), scale); return value >= 0 ? `+${formatted}` : `-${formatted}`; } const formatted = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2 }).format(Math.abs(value)); return value >= 0 ? `+${formatted}` : `-${formatted}`; } function tickerPillClass(disabled?: boolean) { return cn( 'inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs', disabled ? 'border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] text-[color:var(--terminal-muted)]' : 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]' ); } function ControlSection(props: { label: string; value: T; options: Array<{ value: T; label: string }>; onChange: (value: T) => void; ariaLabel: string; }) { return (

{props.label}

{props.options.map((option) => ( ))}
); } function ComparisonTooltip(props: { active?: boolean; payload?: TooltipEntry[]; metric: GraphMetricDefinition; scale: NumberScaleUnit; }) { if (!props.active || !props.payload || props.payload.length === 0) { return null; } const datum = props.payload[0]?.payload; if (!datum) { return null; } const entries = props.payload .filter((entry) => typeof entry.dataKey === 'string') .map((entry) => { const ticker = entry.dataKey as string; const meta = datum[`meta__${ticker}`] as GraphingSeriesPoint | undefined; return { ticker, color: entry.color ?? CHART_MUTED, value: typeof entry.value === 'number' ? entry.value : null, meta }; }); return (

{formatLongDate(entries[0]?.meta?.dateKey ?? null)}

{entries.map((entry) => (
{formatMetricValue(entry.value, props.metric.unit, props.scale)}
{entry.meta ? (

{entry.meta.filingType} · {entry.meta.periodLabel}

) : null}
))}
); } export default function GraphingPage() { return ( Loading graphing desk...}> ); } function GraphingPageContent() { const { isPending, isAuthenticated } = useAuthGuard(); const searchParams = useSearchParams(); const router = useRouter(); const queryClient = useQueryClient(); const { prefetchResearchTicker } = useLinkPrefetch(); const graphState = useMemo(() => parseGraphingParams(searchParams), [searchParams]); const canonicalQuery = useMemo(() => serializeGraphingParams(graphState), [graphState]); const currentQuery = searchParams.toString(); const [tickerInput, setTickerInput] = useState(graphState.tickers.join(', ')); const [results, setResults] = useState([]); const [loading, setLoading] = useState(true); const [refreshNonce, setRefreshNonce] = useState(0); useEffect(() => { setTickerInput(graphState.tickers.join(', ')); }, [graphState.tickers]); useEffect(() => { if (currentQuery !== canonicalQuery) { router.replace(`/graphing?${canonicalQuery}`, { scroll: false }); } }, [canonicalQuery, currentQuery, router]); const replaceGraphState = useCallback((patch: Partial) => { const nextSurface = patch.surface ?? graphState.surface; const nextCadence = patch.cadence ?? graphState.cadence; const nextTickers = patch.tickers && patch.tickers.length > 0 ? patch.tickers : graphState.tickers; const nextMetric = resolveGraphMetric( nextSurface, nextCadence, patch.metric ?? graphState.metric ); const nextState: GraphingUrlState = { tickers: nextTickers, surface: nextSurface, cadence: nextCadence, metric: nextMetric, chart: patch.chart ?? graphState.chart, scale: patch.scale ?? graphState.scale }; router.replace(`/graphing?${serializeGraphingParams(nextState)}`, { scroll: false }); }, [graphState, router]); useEffect(() => { if (isPending || !isAuthenticated) { return; } let cancelled = false; async function loadComparisonSet() { setLoading(true); const settled = await Promise.allSettled(graphState.tickers.map(async (ticker) => { const response = await queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({ ticker, surfaceKind: graphState.surface, cadence: graphState.cadence, includeDimensions: false, includeFacts: false, limit: 16 })); return response.financials; })); if (cancelled) { return; } setResults(settled.map((entry, index) => { const ticker = graphState.tickers[index] ?? `ticker-${index + 1}`; if (entry.status === 'fulfilled') { return { ticker, financials: entry.value } satisfies GraphingFetchResult; } const reason = entry.reason instanceof ApiError ? entry.reason.message : entry.reason instanceof Error ? entry.reason.message : 'Unable to load financial history'; return { ticker, error: reason } satisfies GraphingFetchResult; })); setLoading(false); } void loadComparisonSet(); return () => { cancelled = true; }; }, [graphState, isAuthenticated, isPending, queryClient, refreshNonce]); const metricOptions = useMemo(() => metricsForSurfaceAndCadence(graphState.surface, graphState.cadence), [graphState.cadence, graphState.surface]); const selectedMetric = useMemo( () => getGraphMetricDefinition(graphState.surface, graphState.cadence, graphState.metric), [graphState.cadence, graphState.metric, graphState.surface] ); const comparison = useMemo(() => buildGraphingComparisonData({ results, surface: graphState.surface, metric: graphState.metric }), [graphState.metric, graphState.surface, results]); const hasCurrencyScale = selectedMetric?.unit === 'currency'; if (isPending || !isAuthenticated) { return
Loading graphing desk...
; } return ( )} >
{ event.preventDefault(); const nextTickers = normalizeGraphTickers(tickerInput); replaceGraphState({ tickers: nextTickers.length > 0 ? nextTickers : [...graphState.tickers] }); }} >
setTickerInput(event.target.value.toUpperCase())} placeholder="MSFT, AAPL, NVDA" className="min-w-[260px] flex-1" /> { if (graphState.tickers[0]) { prefetchResearchTicker(graphState.tickers[0]); } }} onFocus={() => { if (graphState.tickers[0]) { prefetchResearchTicker(graphState.tickers[0]); } }} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open canonical graphing URL
{graphState.tickers.map((ticker, index) => ( {ticker} ))}
({ value: value as GraphingUrlState['surface'], label }))} onChange={(value) => replaceGraphState({ surface: value })} /> replaceGraphState({ cadence: value as FinancialCadence })} /> replaceGraphState({ chart: value as GraphChartKind })} /> {hasCurrencyScale ? ( replaceGraphState({ scale: value as NumberScaleUnit })} /> ) : null}

Metric

{selectedMetric ? (

{selectedMetric.label}

{GRAPH_SURFACE_LABELS[selectedMetric.surface]} · {selectedMetric.category.replace(/_/g, ' ')} · unit {selectedMetric.unit}

) : null}
{loading ? (

Loading comparison chart...

) : !selectedMetric || !comparison.hasAnyData ? (

No chart data available for the selected compare set.

Try a different metric, cadence, or company basket.

) : ( <> {comparison.hasPartialData ? (
Partial coverage detected. Some companies are missing values for this metric or failed to load, but the remaining series still render.
) : null}
{comparison.companies.map((company, index) => ( ))}
{graphState.chart === 'line' ? ( formatShortDate(value)} /> formatMetricValue(value, selectedMetric.unit, graphState.scale)} /> } /> {comparison.companies.map((company, index) => ( ))} ) : ( formatShortDate(value)} /> formatMetricValue(value, selectedMetric.unit, graphState.scale)} /> } /> {comparison.companies.map((company, index) => ( ))} )}
)}
{comparison.latestRows.map((row, index) => ( ))}
Company Latest Prior Change Period Filing Status
{row.companyName}
{selectedMetric ? formatMetricValue(row.latestValue, selectedMetric.unit, graphState.scale) : 'n/a'} {selectedMetric ? formatMetricValue(row.priorValue, selectedMetric.unit, graphState.scale) : 'n/a'} = 0 ? 'text-[color:var(--accent)]' : row.changeValue !== null ? 'text-[#ffb5b5]' : 'text-[color:var(--terminal-muted)]')}> {selectedMetric ? formatChangeValue(row.changeValue, selectedMetric.unit, graphState.scale) : 'n/a'} {formatLongDate(row.latestDateKey)} {row.latestPeriodLabel ? · {row.latestPeriodLabel} : null} {row.latestFilingType ?? 'n/a'} {row.status === 'ready' ? ( Ready ) : row.status === 'no_metric_data' ? ( No metric data ) : ( {row.errorMessage ?? 'Load failed'} )}
); }