199 lines
6.2 KiB
TypeScript
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
|
|
};
|
|
}
|