Files
Neon-Desk/lib/graphing/catalog.ts

239 lines
7.5 KiB
TypeScript

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<GraphableFinancialSurfaceKind, string> = {
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<GraphableFinancialSurfaceKind, 'income_statement' | 'balance_sheet' | 'cash_flow_statement'>,
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<GraphableFinancialSurfaceKind, GraphMetricDefinition[]> = {
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<GraphableFinancialSurfaceKind, string> = {
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<string>();
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}`;
}