feat(financials): add compact surface UI and graphing states
This commit is contained in:
@@ -36,6 +36,15 @@ describe('graphing catalog', () => {
|
||||
expect(quarterlyMetricKeys).toContain('gross_margin');
|
||||
});
|
||||
|
||||
it('includes other operating expense in the income statement metric catalog', () => {
|
||||
const metricKeys = metricsForSurfaceAndCadence('income_statement', 'annual').map((metric) => metric.key);
|
||||
|
||||
expect(metricKeys).toContain('operating_expenses');
|
||||
expect(metricKeys).toContain('selling_general_and_administrative');
|
||||
expect(metricKeys).toContain('research_and_development');
|
||||
expect(metricKeys).toContain('other_operating_expense');
|
||||
});
|
||||
|
||||
it('replaces invalid metrics after surface and cadence normalization', () => {
|
||||
const state = parseGraphingParams(new URLSearchParams('surface=ratios&cadence=quarterly&metric=5y_revenue_cagr&tickers=msft,aapl'));
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
FinancialStatementPeriod,
|
||||
RatioRow,
|
||||
StandardizedFinancialRow
|
||||
SurfaceFinancialRow
|
||||
} from '@/lib/types';
|
||||
|
||||
function createPeriod(input: {
|
||||
@@ -26,7 +26,7 @@ function createPeriod(input: {
|
||||
} satisfies FinancialStatementPeriod;
|
||||
}
|
||||
|
||||
function createStatementRow(key: string, values: Record<string, number | null>, unit: StandardizedFinancialRow['unit'] = 'currency') {
|
||||
function createStatementRow(key: string, values: Record<string, number | null>, unit: SurfaceFinancialRow['unit'] = 'currency') {
|
||||
return {
|
||||
key,
|
||||
label: key,
|
||||
@@ -39,8 +39,9 @@ function createStatementRow(key: string, values: Record<string, number | null>,
|
||||
sourceFactIds: [1],
|
||||
formulaKey: null,
|
||||
hasDimensions: false,
|
||||
resolvedSourceRowKeys: Object.fromEntries(Object.keys(values).map((periodId) => [periodId, key]))
|
||||
} satisfies StandardizedFinancialRow;
|
||||
resolvedSourceRowKeys: Object.fromEntries(Object.keys(values).map((periodId) => [periodId, key])),
|
||||
statement: 'income'
|
||||
} satisfies SurfaceFinancialRow;
|
||||
}
|
||||
|
||||
function createRatioRow(key: string, values: Record<string, number | null>, unit: RatioRow['unit'] = 'percent') {
|
||||
@@ -54,8 +55,9 @@ function createFinancials(input: {
|
||||
ticker: string;
|
||||
companyName: string;
|
||||
periods: FinancialStatementPeriod[];
|
||||
statementRows?: StandardizedFinancialRow[];
|
||||
statementRows?: SurfaceFinancialRow[];
|
||||
ratioRows?: RatioRow[];
|
||||
fiscalPack?: string | null;
|
||||
}) {
|
||||
return {
|
||||
company: {
|
||||
@@ -72,6 +74,7 @@ function createFinancials(input: {
|
||||
faithful: [],
|
||||
standardized: input.statementRows ?? []
|
||||
},
|
||||
statementDetails: null,
|
||||
ratioRows: input.ratioRows ?? [],
|
||||
kpiRows: null,
|
||||
trendSeries: [],
|
||||
@@ -100,6 +103,13 @@ function createFinancials(input: {
|
||||
taxonomy: null,
|
||||
validation: null
|
||||
},
|
||||
normalization: {
|
||||
regime: 'unknown',
|
||||
fiscalPack: input.fiscalPack ?? null,
|
||||
parserVersion: '0.0.0',
|
||||
unmappedRowCount: 0,
|
||||
materialUnmappedRowCount: 0
|
||||
},
|
||||
dimensionBreakdown: null
|
||||
} satisfies CompanyFinancialStatementsResponse;
|
||||
}
|
||||
@@ -194,6 +204,37 @@ describe('graphing series', () => {
|
||||
expect(data.hasAnyData).toBe(false);
|
||||
});
|
||||
|
||||
it('marks not meaningful standardized rows separately from missing metric data', () => {
|
||||
const data = buildGraphingComparisonData({
|
||||
surface: 'income_statement',
|
||||
metric: 'gross_profit',
|
||||
results: [
|
||||
{
|
||||
ticker: 'JPM',
|
||||
financials: createFinancials({
|
||||
ticker: 'JPM',
|
||||
companyName: 'JPMorgan Chase & Co.',
|
||||
fiscalPack: 'bank_lender',
|
||||
periods: [createPeriod({ id: 'jpm-fy', filingId: 1, filingDate: '2026-02-13', periodEnd: '2025-12-31', filingType: '10-K' })],
|
||||
statementRows: [{
|
||||
...createStatementRow('gross_profit', { 'jpm-fy': null }),
|
||||
resolutionMethod: 'not_meaningful'
|
||||
}]
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(data.latestRows[0]).toMatchObject({
|
||||
ticker: 'JPM',
|
||||
fiscalPack: 'bank_lender',
|
||||
status: 'not_meaningful',
|
||||
errorMessage: 'Not meaningful for this pack.'
|
||||
});
|
||||
expect(data.hasAnyData).toBe(false);
|
||||
expect(data.hasPartialData).toBe(true);
|
||||
});
|
||||
|
||||
it('derives latest and prior values for the summary table', () => {
|
||||
const data = buildGraphingComparisonData({
|
||||
surface: 'income_statement',
|
||||
@@ -216,6 +257,7 @@ describe('graphing series', () => {
|
||||
|
||||
expect(data.latestRows[0]).toMatchObject({
|
||||
ticker: 'AMD',
|
||||
fiscalPack: null,
|
||||
latestValue: 70,
|
||||
priorValue: 50,
|
||||
changeValue: 20,
|
||||
|
||||
@@ -27,7 +27,8 @@ export type GraphingSeriesPoint = {
|
||||
export type GraphingCompanySeries = {
|
||||
ticker: string;
|
||||
companyName: string;
|
||||
status: 'ready' | 'error' | 'no_metric_data';
|
||||
fiscalPack: string | null;
|
||||
status: 'ready' | 'error' | 'no_metric_data' | 'not_meaningful';
|
||||
errorMessage: string | null;
|
||||
unit: FinancialUnit | null;
|
||||
points: GraphingSeriesPoint[];
|
||||
@@ -38,6 +39,7 @@ export type GraphingCompanySeries = {
|
||||
export type GraphingLatestValueRow = {
|
||||
ticker: string;
|
||||
companyName: string;
|
||||
fiscalPack: string | null;
|
||||
status: GraphingCompanySeries['status'];
|
||||
errorMessage: string | null;
|
||||
latestValue: number | null;
|
||||
@@ -86,6 +88,7 @@ function extractCompanySeries(
|
||||
return {
|
||||
ticker: result.ticker,
|
||||
companyName: result.ticker,
|
||||
fiscalPack: null,
|
||||
status: 'error',
|
||||
errorMessage: result.error ?? 'Unable to load financial history',
|
||||
unit: null,
|
||||
@@ -97,6 +100,10 @@ function extractCompanySeries(
|
||||
|
||||
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,
|
||||
@@ -113,8 +120,9 @@ function extractCompanySeries(
|
||||
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.',
|
||||
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,
|
||||
@@ -159,6 +167,7 @@ export function buildGraphingComparisonData(input: {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user