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

199 lines
6.2 KiB
TypeScript

import type {
CompanyFinancialStatementsResponse,
FinancialUnit,
RatioRow,
StandardizedFinancialRow
} from '@/lib/types';
import type { GraphableFinancialSurfaceKind } from '@/lib/financial-metrics';
type GraphingMetricRow = StandardizedFinancialRow | RatioRow;
export type GraphingFetchResult = {
ticker: string;
financials?: CompanyFinancialStatementsResponse;
error?: string;
};
export type GraphingSeriesPoint = {
periodId: string;
dateKey: string;
filingType: '10-K' | '10-Q';
filingDate: string;
periodEnd: string | null;
periodLabel: string;
value: number | null;
};
export type GraphingCompanySeries = {
ticker: string;
companyName: string;
fiscalPack: string | null;
status: 'ready' | 'error' | 'no_metric_data' | 'not_meaningful';
errorMessage: string | null;
unit: FinancialUnit | null;
points: GraphingSeriesPoint[];
latestPoint: GraphingSeriesPoint | null;
priorPoint: GraphingSeriesPoint | null;
};
export type GraphingLatestValueRow = {
ticker: string;
companyName: string;
fiscalPack: string | null;
status: GraphingCompanySeries['status'];
errorMessage: string | null;
latestValue: number | null;
priorValue: number | null;
changeValue: number | null;
latestDateKey: string | null;
latestPeriodLabel: string | null;
latestFilingType: '10-K' | '10-Q' | null;
};
export type GraphingChartDatum = Record<string, unknown> & {
dateKey: string;
dateMs: number;
};
type GraphingComparisonData = {
companies: GraphingCompanySeries[];
chartData: GraphingChartDatum[];
latestRows: GraphingLatestValueRow[];
hasAnyData: boolean;
hasPartialData: boolean;
};
function sortPeriods(left: { periodEnd: string | null; filingDate: string }, right: { periodEnd: string | null; filingDate: string }) {
return Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate);
}
function extractMetricRow(
financials: CompanyFinancialStatementsResponse,
surface: GraphableFinancialSurfaceKind,
metric: string
): GraphingMetricRow | null {
if (surface === 'ratios') {
return financials.ratioRows?.find((row) => row.key === metric) ?? null;
}
return financials.statementRows?.standardized.find((row) => row.key === metric) ?? null;
}
function extractCompanySeries(
result: GraphingFetchResult,
surface: GraphableFinancialSurfaceKind,
metric: string
): GraphingCompanySeries {
if (result.error || !result.financials) {
return {
ticker: result.ticker,
companyName: result.ticker,
fiscalPack: null,
status: 'error',
errorMessage: result.error ?? 'Unable to load financial history',
unit: null,
points: [],
latestPoint: null,
priorPoint: null
};
}
const metricRow = extractMetricRow(result.financials, surface, metric);
const periods = [...result.financials.periods].sort(sortPeriods);
const notMeaningful = surface !== 'ratios'
&& metricRow
&& 'resolutionMethod' in metricRow
&& metricRow.resolutionMethod === 'not_meaningful';
const points = periods.map((period) => ({
periodId: period.id,
dateKey: period.periodEnd ?? period.filingDate,
filingType: period.filingType,
filingDate: period.filingDate,
periodEnd: period.periodEnd,
periodLabel: period.periodLabel,
value: metricRow?.values[period.id] ?? null
}));
const populatedPoints = points.filter((point) => point.value !== null);
const latestPoint = populatedPoints[populatedPoints.length - 1] ?? null;
const priorPoint = populatedPoints.length > 1 ? populatedPoints[populatedPoints.length - 2] ?? null : null;
return {
ticker: result.financials.company.ticker,
companyName: result.financials.company.companyName,
fiscalPack: result.financials.normalization.fiscalPack,
status: notMeaningful ? 'not_meaningful' : latestPoint ? 'ready' : 'no_metric_data',
errorMessage: notMeaningful ? 'Not meaningful for this pack.' : latestPoint ? null : 'No data available for the selected metric.',
unit: metricRow?.unit ?? null,
points,
latestPoint,
priorPoint
};
}
export function buildGraphingComparisonData(input: {
results: GraphingFetchResult[];
surface: GraphableFinancialSurfaceKind;
metric: string;
}): GraphingComparisonData {
const companies = input.results.map((result) => extractCompanySeries(result, input.surface, input.metric));
const chartDatumByDate = new Map<string, GraphingChartDatum>();
for (const company of companies) {
for (const point of company.points) {
const dateMs = Date.parse(point.dateKey);
const existing = chartDatumByDate.get(point.dateKey) ?? {
dateKey: point.dateKey,
dateMs
};
existing[company.ticker] = point.value;
existing[`meta__${company.ticker}`] = point;
chartDatumByDate.set(point.dateKey, existing);
}
}
const chartData = [...chartDatumByDate.values()]
.sort((left, right) => left.dateMs - right.dateMs)
.map((datum) => {
for (const company of companies) {
if (!(company.ticker in datum)) {
datum[company.ticker] = null;
}
}
return datum;
});
const latestRows = companies.map((company) => ({
ticker: company.ticker,
companyName: company.companyName,
fiscalPack: company.fiscalPack,
status: company.status,
errorMessage: company.errorMessage,
latestValue: company.latestPoint?.value ?? null,
priorValue: company.priorPoint?.value ?? null,
changeValue:
company.latestPoint?.value !== null
&& company.latestPoint?.value !== undefined
&& company.priorPoint?.value !== null
&& company.priorPoint?.value !== undefined
? company.latestPoint.value - company.priorPoint.value
: null,
latestDateKey: company.latestPoint?.dateKey ?? null,
latestPeriodLabel: company.latestPoint?.periodLabel ?? null,
latestFilingType: company.latestPoint?.filingType ?? null
}));
const hasAnyData = companies.some((company) => company.latestPoint !== null);
const hasPartialData = companies.some((company) => company.status !== 'ready')
|| companies.some((company) => company.points.some((point) => point.value === null));
return {
companies,
chartData,
latestRows,
hasAnyData,
hasPartialData
};
}