Add research workspace and graphing flows
This commit is contained in:
238
lib/graphing/catalog.ts
Normal file
238
lib/graphing/catalog.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user