import type { FinancialCadence, FinancialUnit, NumberScaleUnit } from '@/lib/types'; import { BALANCE_SHEET_METRIC_DEFINITIONS, CASH_FLOW_STATEMENT_METRIC_DEFINITIONS, GRAPHABLE_FINANCIAL_SURFACES, type GraphableFinancialSurfaceKind, INCOME_STATEMENT_METRIC_DEFINITIONS, RATIO_DEFINITIONS } from '@/lib/financial-metrics'; type SearchParamsLike = { get(name: string): string | null; }; export type GraphChartKind = 'line' | 'bar'; export type GraphMetricDefinition = { surface: GraphableFinancialSurfaceKind; key: string; label: string; category: string; order: number; unit: FinancialUnit; supportedCadences: readonly FinancialCadence[]; }; export type GraphingUrlState = { tickers: string[]; surface: GraphableFinancialSurfaceKind; metric: string; cadence: FinancialCadence; chart: GraphChartKind; scale: NumberScaleUnit; }; export const DEFAULT_GRAPH_TICKERS = ['MSFT', 'AAPL', 'NVDA'] as const; const DEFAULT_GRAPH_SURFACE: GraphableFinancialSurfaceKind = 'income_statement'; const DEFAULT_GRAPH_CADENCE: FinancialCadence = 'annual'; const DEFAULT_GRAPH_CHART: GraphChartKind = 'line'; const DEFAULT_GRAPH_SCALE: NumberScaleUnit = 'millions'; export const GRAPH_SURFACE_LABELS: Record = { income_statement: 'Income Statement', balance_sheet: 'Balance Sheet', cash_flow_statement: 'Cash Flow Statement', ratios: 'Ratios' }; export const GRAPH_CADENCE_OPTIONS: Array<{ value: FinancialCadence; label: string }> = [ { value: 'annual', label: 'Annual' }, { value: 'quarterly', label: 'Quarterly' }, { value: 'ltm', label: 'LTM' } ]; export const GRAPH_CHART_OPTIONS: Array<{ value: GraphChartKind; label: string }> = [ { value: 'line', label: 'Line' }, { value: 'bar', label: 'Bar' } ]; export const GRAPH_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [ { value: 'thousands', label: 'Thousands (K)' }, { value: 'millions', label: 'Millions (M)' }, { value: 'billions', label: 'Billions (B)' } ]; function buildStatementMetrics( surface: Extract, metrics: Array<{ key: string; label: string; category: string; order: number; unit: FinancialUnit; }> ) { return metrics.map((metric) => ({ ...metric, surface, supportedCadences: ['annual', 'quarterly', 'ltm'] as const })) satisfies GraphMetricDefinition[]; } const GRAPH_METRIC_CATALOG: Record = { income_statement: buildStatementMetrics('income_statement', INCOME_STATEMENT_METRIC_DEFINITIONS), balance_sheet: buildStatementMetrics('balance_sheet', BALANCE_SHEET_METRIC_DEFINITIONS), cash_flow_statement: buildStatementMetrics('cash_flow_statement', CASH_FLOW_STATEMENT_METRIC_DEFINITIONS), ratios: RATIO_DEFINITIONS.map((metric) => ({ surface: 'ratios', key: metric.key, label: metric.label, category: metric.category, order: metric.order, unit: metric.unit, supportedCadences: metric.supportedCadences ?? ['annual', 'quarterly', 'ltm'] })) }; const DEFAULT_GRAPH_METRIC_BY_SURFACE: Record = { income_statement: 'revenue', balance_sheet: 'total_assets', cash_flow_statement: 'free_cash_flow', ratios: 'gross_margin' }; export function normalizeGraphTickers(value: string | null | undefined) { const raw = (value ?? '') .split(',') .map((entry) => entry.trim().toUpperCase()) .filter(Boolean); const unique = new Set(); for (const ticker of raw) { unique.add(ticker); if (unique.size >= 5) { break; } } return [...unique]; } function isGraphSurfaceKind(value: string | null | undefined): value is GraphableFinancialSurfaceKind { return GRAPHABLE_FINANCIAL_SURFACES.includes(value as GraphableFinancialSurfaceKind); } function isGraphCadence(value: string | null | undefined): value is FinancialCadence { return value === 'annual' || value === 'quarterly' || value === 'ltm'; } function isGraphChartKind(value: string | null | undefined): value is GraphChartKind { return value === 'line' || value === 'bar'; } function isNumberScaleUnit(value: string | null | undefined): value is NumberScaleUnit { return value === 'thousands' || value === 'millions' || value === 'billions'; } export function metricsForSurfaceAndCadence( surface: GraphableFinancialSurfaceKind, cadence: FinancialCadence ) { return GRAPH_METRIC_CATALOG[surface].filter((metric) => metric.supportedCadences.includes(cadence)); } export function resolveGraphMetric( surface: GraphableFinancialSurfaceKind, cadence: FinancialCadence, metric: string | null | undefined ) { const metrics = metricsForSurfaceAndCadence(surface, cadence); const normalizedMetric = metric?.trim() ?? ''; const match = metrics.find((candidate) => candidate.key === normalizedMetric); if (match) { return match.key; } const surfaceDefault = metrics.find((candidate) => candidate.key === DEFAULT_GRAPH_METRIC_BY_SURFACE[surface]); return surfaceDefault?.key ?? metrics[0]?.key ?? DEFAULT_GRAPH_METRIC_BY_SURFACE[surface]; } export function getGraphMetricDefinition( surface: GraphableFinancialSurfaceKind, cadence: FinancialCadence, metric: string ) { return metricsForSurfaceAndCadence(surface, cadence).find((candidate) => candidate.key === metric) ?? null; } function defaultGraphingState(): GraphingUrlState { return { tickers: [...DEFAULT_GRAPH_TICKERS], surface: DEFAULT_GRAPH_SURFACE, metric: DEFAULT_GRAPH_METRIC_BY_SURFACE[DEFAULT_GRAPH_SURFACE], cadence: DEFAULT_GRAPH_CADENCE, chart: DEFAULT_GRAPH_CHART, scale: DEFAULT_GRAPH_SCALE }; } export function parseGraphingParams(searchParams: SearchParamsLike): GraphingUrlState { const tickers = normalizeGraphTickers(searchParams.get('tickers')); const surface = isGraphSurfaceKind(searchParams.get('surface')) ? searchParams.get('surface') as GraphableFinancialSurfaceKind : DEFAULT_GRAPH_SURFACE; const cadence = isGraphCadence(searchParams.get('cadence')) ? searchParams.get('cadence') as FinancialCadence : DEFAULT_GRAPH_CADENCE; const metric = resolveGraphMetric(surface, cadence, searchParams.get('metric')); const chart = isGraphChartKind(searchParams.get('chart')) ? searchParams.get('chart') as GraphChartKind : DEFAULT_GRAPH_CHART; const scale = isNumberScaleUnit(searchParams.get('scale')) ? searchParams.get('scale') as NumberScaleUnit : DEFAULT_GRAPH_SCALE; return { tickers: tickers.length > 0 ? tickers : [...DEFAULT_GRAPH_TICKERS], surface, metric, cadence, chart, scale }; } export function serializeGraphingParams(state: GraphingUrlState) { const params = new URLSearchParams(); params.set('tickers', state.tickers.join(',')); params.set('surface', state.surface); params.set('metric', state.metric); params.set('cadence', state.cadence); params.set('chart', state.chart); params.set('scale', state.scale); return params.toString(); } function withPrimaryGraphTicker(ticker: string | null | undefined) { const normalized = ticker?.trim().toUpperCase() ?? ''; if (!normalized) { return [...DEFAULT_GRAPH_TICKERS]; } return normalizeGraphTickers([normalized, ...DEFAULT_GRAPH_TICKERS].join(',')); } export function buildGraphingHref(primaryTicker?: string | null) { const tickers = withPrimaryGraphTicker(primaryTicker); const params = serializeGraphingParams({ ...defaultGraphingState(), tickers }); return `/graphing?${params}`; }