239 lines
7.6 KiB
TypeScript
239 lines
7.6 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;
|
|
export const DEFAULT_GRAPH_SURFACE: GraphableFinancialSurfaceKind = 'income_statement';
|
|
export const DEFAULT_GRAPH_CADENCE: FinancialCadence = 'annual';
|
|
export const DEFAULT_GRAPH_CHART: GraphChartKind = 'line';
|
|
export 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[];
|
|
}
|
|
|
|
export 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']
|
|
}))
|
|
};
|
|
|
|
export 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];
|
|
}
|
|
|
|
export function isGraphSurfaceKind(value: string | null | undefined): value is GraphableFinancialSurfaceKind {
|
|
return GRAPHABLE_FINANCIAL_SURFACES.includes(value as GraphableFinancialSurfaceKind);
|
|
}
|
|
|
|
export function isGraphCadence(value: string | null | undefined): value is FinancialCadence {
|
|
return value === 'annual' || value === 'quarterly' || value === 'ltm';
|
|
}
|
|
|
|
export function isGraphChartKind(value: string | null | undefined): value is GraphChartKind {
|
|
return value === 'line' || value === 'bar';
|
|
}
|
|
|
|
export 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;
|
|
}
|
|
|
|
export 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();
|
|
}
|
|
|
|
export 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}`;
|
|
}
|