Add research workspace and graphing flows
This commit is contained in:
189
lib/graphing/series.ts
Normal file
189
lib/graphing/series.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
status: 'ready' | 'error' | 'no_metric_data';
|
||||
errorMessage: string | null;
|
||||
unit: FinancialUnit | null;
|
||||
points: GraphingSeriesPoint[];
|
||||
latestPoint: GraphingSeriesPoint | null;
|
||||
priorPoint: GraphingSeriesPoint | null;
|
||||
};
|
||||
|
||||
export type GraphingLatestValueRow = {
|
||||
ticker: string;
|
||||
companyName: string;
|
||||
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;
|
||||
};
|
||||
|
||||
export 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,
|
||||
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 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,
|
||||
status: latestPoint ? 'ready' : 'no_metric_data',
|
||||
errorMessage: 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,
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user