Implement dual-surface financials and db bootstrap
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
@@ -17,7 +17,15 @@ import {
|
||||
XAxis,
|
||||
YAxis
|
||||
} from 'recharts';
|
||||
import { AlertTriangle, ChartNoAxesCombined, ChevronDown, Download, RefreshCcw, Search } from 'lucide-react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChartNoAxesCombined,
|
||||
ChevronDown,
|
||||
Download,
|
||||
GitCompareArrows,
|
||||
RefreshCcw,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
import { AppShell } from '@/components/shell/app-shell';
|
||||
import { MetricCard } from '@/components/dashboard/metric-card';
|
||||
import {
|
||||
@@ -42,7 +50,9 @@ import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
DimensionBreakdownRow,
|
||||
FinancialHistoryWindow,
|
||||
FinancialStatementSurfaceKind,
|
||||
FinancialStatementKind,
|
||||
StandardizedStatementRow,
|
||||
TaxonomyStatementRow
|
||||
} from '@/lib/types';
|
||||
|
||||
@@ -51,6 +61,20 @@ type LoadOptions = {
|
||||
append?: boolean;
|
||||
};
|
||||
|
||||
type OverviewPoint = {
|
||||
periodId: string;
|
||||
filingDate: string;
|
||||
periodEnd: string | null;
|
||||
label: string;
|
||||
revenue: number | null;
|
||||
netIncome: number | null;
|
||||
totalAssets: number | null;
|
||||
cash: number | null;
|
||||
debt: number | null;
|
||||
};
|
||||
|
||||
type DisplayRow = TaxonomyStatementRow | StandardizedStatementRow;
|
||||
|
||||
const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [
|
||||
{ value: 'thousands', label: 'Thousands (K)' },
|
||||
{ value: 'millions', label: 'Millions (M)' },
|
||||
@@ -70,23 +94,16 @@ const WINDOW_OPTIONS: Array<{ value: FinancialHistoryWindow; label: string }> =
|
||||
{ value: 'all', label: 'Full Available' }
|
||||
];
|
||||
|
||||
const SURFACE_OPTIONS: Array<{ value: FinancialStatementSurfaceKind; label: string }> = [
|
||||
{ value: 'standardized', label: 'Standardized' },
|
||||
{ value: 'faithful', label: 'Filing-faithful' }
|
||||
];
|
||||
|
||||
const CHART_MUTED = '#b4ced9';
|
||||
const CHART_GRID = 'rgba(126, 217, 255, 0.24)';
|
||||
const CHART_TOOLTIP_BG = 'rgba(6, 17, 24, 0.95)';
|
||||
const CHART_TOOLTIP_BORDER = 'rgba(123, 255, 217, 0.45)';
|
||||
|
||||
type OverviewPoint = {
|
||||
periodId: string;
|
||||
filingDate: string;
|
||||
periodEnd: string | null;
|
||||
label: string;
|
||||
revenue: number | null;
|
||||
netIncome: number | null;
|
||||
totalAssets: number | null;
|
||||
cash: number | null;
|
||||
debt: number | null;
|
||||
};
|
||||
|
||||
function formatLongDate(value: string) {
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
@@ -134,6 +151,33 @@ function rowValue(row: { values: Record<string, number | null> }, periodId: stri
|
||||
return periodId in row.values ? row.values[periodId] : null;
|
||||
}
|
||||
|
||||
function isFaithfulRow(row: DisplayRow): row is TaxonomyStatementRow {
|
||||
return 'localName' in row;
|
||||
}
|
||||
|
||||
function isStandardizedRow(row: DisplayRow): row is StandardizedStatementRow {
|
||||
return 'sourceRowKeys' in row;
|
||||
}
|
||||
|
||||
function mergeSurfaceRows<T extends { key: string; values: Record<string, number | null>; hasDimensions: boolean }>(
|
||||
rows: T[],
|
||||
mergeRow: (existing: T, row: T) => void
|
||||
) {
|
||||
const rowMap = new Map<string, T>();
|
||||
|
||||
for (const row of rows) {
|
||||
const existing = rowMap.get(row.key);
|
||||
if (!existing) {
|
||||
rowMap.set(row.key, structuredClone(row));
|
||||
continue;
|
||||
}
|
||||
|
||||
mergeRow(existing, row);
|
||||
}
|
||||
|
||||
return [...rowMap.values()];
|
||||
}
|
||||
|
||||
function mergeFinancialPages(
|
||||
base: CompanyFinancialStatementsResponse | null,
|
||||
next: CompanyFinancialStatementsResponse
|
||||
@@ -146,42 +190,85 @@ function mergeFinancialPages(
|
||||
.filter((period, index, list) => list.findIndex((item) => item.id === period.id) === index)
|
||||
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
|
||||
|
||||
const rowMap = new Map<string, TaxonomyStatementRow>();
|
||||
const faithfulRows = mergeSurfaceRows(
|
||||
[...base.surfaces.faithful.rows, ...next.surfaces.faithful.rows],
|
||||
(existing, row) => {
|
||||
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
||||
existing.order = Math.min(existing.order, row.order);
|
||||
existing.depth = Math.min(existing.depth, row.depth);
|
||||
if (!existing.parentKey && row.parentKey) {
|
||||
existing.parentKey = row.parentKey;
|
||||
}
|
||||
|
||||
for (const row of [...base.rows, ...next.rows]) {
|
||||
const existing = rowMap.get(row.key);
|
||||
if (!existing) {
|
||||
rowMap.set(row.key, {
|
||||
...row,
|
||||
values: { ...row.values },
|
||||
units: { ...row.units },
|
||||
sourceFactIds: [...row.sourceFactIds]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const [periodId, value] of Object.entries(row.values)) {
|
||||
if (!(periodId in existing.values)) {
|
||||
existing.values[periodId] = value;
|
||||
}
|
||||
}
|
||||
|
||||
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
||||
existing.order = Math.min(existing.order, row.order);
|
||||
existing.depth = Math.min(existing.depth, row.depth);
|
||||
for (const [periodId, unit] of Object.entries(row.units)) {
|
||||
if (!(periodId in existing.units)) {
|
||||
existing.units[periodId] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [periodId, value] of Object.entries(row.values)) {
|
||||
if (!(periodId in existing.values)) {
|
||||
existing.values[periodId] = value;
|
||||
for (const factId of row.sourceFactIds) {
|
||||
if (!existing.sourceFactIds.includes(factId)) {
|
||||
existing.sourceFactIds.push(factId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [periodId, unit] of Object.entries(row.units)) {
|
||||
if (!(periodId in existing.units)) {
|
||||
existing.units[periodId] = unit;
|
||||
}
|
||||
).sort((left, right) => {
|
||||
if (left.order !== right.order) {
|
||||
return left.order - right.order;
|
||||
}
|
||||
|
||||
for (const factId of row.sourceFactIds) {
|
||||
if (!existing.sourceFactIds.includes(factId)) {
|
||||
existing.sourceFactIds.push(factId);
|
||||
return left.label.localeCompare(right.label);
|
||||
});
|
||||
|
||||
const standardizedRows = mergeSurfaceRows(
|
||||
[...base.surfaces.standardized.rows, ...next.surfaces.standardized.rows],
|
||||
(existing, row) => {
|
||||
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
||||
existing.order = Math.min(existing.order, row.order);
|
||||
|
||||
for (const [periodId, value] of Object.entries(row.values)) {
|
||||
if (!(periodId in existing.values)) {
|
||||
existing.values[periodId] = value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [periodId, sourceRowKey] of Object.entries(row.resolvedSourceRowKeys)) {
|
||||
if (!(periodId in existing.resolvedSourceRowKeys)) {
|
||||
existing.resolvedSourceRowKeys[periodId] = sourceRowKey;
|
||||
}
|
||||
}
|
||||
|
||||
for (const concept of row.sourceConcepts) {
|
||||
if (!existing.sourceConcepts.includes(concept)) {
|
||||
existing.sourceConcepts.push(concept);
|
||||
}
|
||||
}
|
||||
|
||||
for (const rowKey of row.sourceRowKeys) {
|
||||
if (!existing.sourceRowKeys.includes(rowKey)) {
|
||||
existing.sourceRowKeys.push(rowKey);
|
||||
}
|
||||
}
|
||||
|
||||
for (const factId of row.sourceFactIds) {
|
||||
if (!existing.sourceFactIds.includes(factId)) {
|
||||
existing.sourceFactIds.push(factId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).sort((left, right) => {
|
||||
if (left.order !== right.order) {
|
||||
return left.order - right.order;
|
||||
}
|
||||
|
||||
return left.label.localeCompare(right.label);
|
||||
});
|
||||
|
||||
const dimensionBreakdown = (() => {
|
||||
if (!base.dimensionBreakdown && !next.dimensionBreakdown) {
|
||||
@@ -210,16 +297,21 @@ function mergeFinancialPages(
|
||||
return {
|
||||
...next,
|
||||
periods,
|
||||
rows: [...rowMap.values()],
|
||||
surfaces: {
|
||||
faithful: {
|
||||
kind: 'faithful' as const,
|
||||
rows: faithfulRows
|
||||
},
|
||||
standardized: {
|
||||
kind: 'standardized' as const,
|
||||
rows: standardizedRows
|
||||
}
|
||||
},
|
||||
nextCursor: next.nextCursor,
|
||||
coverage: {
|
||||
...next.coverage,
|
||||
filings: periods.length,
|
||||
rows: rowMap.size,
|
||||
dimensions: dimensionBreakdown
|
||||
? Object.values(dimensionBreakdown).reduce((total, rows) => total + rows.length, 0)
|
||||
: 0,
|
||||
facts: next.coverage.facts
|
||||
rows: faithfulRows.length
|
||||
},
|
||||
dataSourceStatus: {
|
||||
...next.dataSourceStatus,
|
||||
@@ -229,23 +321,23 @@ function mergeFinancialPages(
|
||||
};
|
||||
}
|
||||
|
||||
function findRowByLocalNames(rows: TaxonomyStatementRow[], localNames: string[]) {
|
||||
const exact = rows.find((row) => localNames.some((name) => row.localName.toLowerCase() === name.toLowerCase()));
|
||||
function findFaithfulRowByLocalNames(rows: TaxonomyStatementRow[], localNames: string[]) {
|
||||
const normalizedNames = localNames.map((name) => name.toLowerCase());
|
||||
const exact = rows.find((row) => normalizedNames.includes(row.localName.toLowerCase()));
|
||||
if (exact) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
const exactLabel = rows.find((row) => localNames.some((name) => row.label.toLowerCase() === name.toLowerCase()));
|
||||
if (exactLabel) {
|
||||
return exactLabel;
|
||||
}
|
||||
|
||||
return rows.find((row) => {
|
||||
const haystack = `${row.key} ${row.label} ${row.localName} ${row.qname}`.toLowerCase();
|
||||
return localNames.some((name) => haystack.includes(name.toLowerCase()));
|
||||
return normalizedNames.some((name) => haystack.includes(name));
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
function findStandardizedRow(rows: StandardizedStatementRow[], key: string) {
|
||||
return rows.find((row) => row.key === key) ?? null;
|
||||
}
|
||||
|
||||
function buildOverviewSeries(
|
||||
incomeData: CompanyFinancialStatementsResponse | null,
|
||||
balanceData: CompanyFinancialStatementsResponse | null
|
||||
@@ -269,24 +361,21 @@ function buildOverviewSeries(
|
||||
}))
|
||||
.sort((a, b) => Date.parse(a.periodEnd ?? a.filingDate) - Date.parse(b.periodEnd ?? b.filingDate));
|
||||
|
||||
const incomeRows = incomeData?.rows ?? [];
|
||||
const balanceRows = balanceData?.rows ?? [];
|
||||
const incomeStandardized = incomeData?.surfaces.standardized.rows ?? [];
|
||||
const balanceStandardized = balanceData?.surfaces.standardized.rows ?? [];
|
||||
const incomeFaithful = incomeData?.surfaces.faithful.rows ?? [];
|
||||
const balanceFaithful = balanceData?.surfaces.faithful.rows ?? [];
|
||||
|
||||
const revenueRow = findRowByLocalNames(incomeRows, [
|
||||
'RevenueFromContractWithCustomerExcludingAssessedTax',
|
||||
'Revenues',
|
||||
'SalesRevenueNet',
|
||||
'Revenue'
|
||||
]);
|
||||
const netIncomeRow = findRowByLocalNames(incomeRows, ['NetIncomeLoss', 'ProfitLoss']);
|
||||
const assetsRow = findRowByLocalNames(balanceRows, ['Assets']);
|
||||
const cashRow = findRowByLocalNames(balanceRows, [
|
||||
'CashAndCashEquivalentsAtCarryingValue',
|
||||
'CashCashEquivalentsAndShortTermInvestments',
|
||||
'CashAndShortTermInvestments',
|
||||
'Cash'
|
||||
]);
|
||||
const debtRow = findRowByLocalNames(balanceRows, ['LongTermDebt', 'Debt', 'LongTermDebtNoncurrent']);
|
||||
const revenueRow = findStandardizedRow(incomeStandardized, 'revenue')
|
||||
?? findFaithfulRowByLocalNames(incomeFaithful, ['RevenueFromContractWithCustomerExcludingAssessedTax', 'Revenues', 'SalesRevenueNet', 'Revenue']);
|
||||
const netIncomeRow = findStandardizedRow(incomeStandardized, 'net-income')
|
||||
?? findFaithfulRowByLocalNames(incomeFaithful, ['NetIncomeLoss', 'ProfitLoss']);
|
||||
const assetsRow = findStandardizedRow(balanceStandardized, 'total-assets')
|
||||
?? findFaithfulRowByLocalNames(balanceFaithful, ['Assets']);
|
||||
const cashRow = findStandardizedRow(balanceStandardized, 'cash-and-equivalents')
|
||||
?? findFaithfulRowByLocalNames(balanceFaithful, ['CashAndCashEquivalentsAtCarryingValue', 'CashAndShortTermInvestments', 'Cash']);
|
||||
const debtRow = findStandardizedRow(balanceStandardized, 'total-debt')
|
||||
?? findFaithfulRowByLocalNames(balanceFaithful, ['LongTermDebt', 'Debt', 'LongTermDebtNoncurrent']);
|
||||
|
||||
return periods.map((period) => ({
|
||||
periodId: period.periodId,
|
||||
@@ -301,6 +390,54 @@ function buildOverviewSeries(
|
||||
}));
|
||||
}
|
||||
|
||||
function groupDimensionRows(
|
||||
rows: DimensionBreakdownRow[],
|
||||
surface: FinancialStatementSurfaceKind
|
||||
) {
|
||||
if (surface === 'faithful') {
|
||||
return rows;
|
||||
}
|
||||
|
||||
return [...rows].sort((left, right) => {
|
||||
if (left.periodId !== right.periodId) {
|
||||
return left.periodId.localeCompare(right.periodId);
|
||||
}
|
||||
|
||||
return `${left.sourceLabel ?? ''}${left.concept ?? ''}`.localeCompare(`${right.sourceLabel ?? ''}${right.concept ?? ''}`);
|
||||
});
|
||||
}
|
||||
|
||||
function ChartFrame({ children }: { children: React.ReactNode }) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const next = entries[0];
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height } = next.contentRect;
|
||||
setReady(width > 0 && height > 0);
|
||||
});
|
||||
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="h-[320px]">
|
||||
{ready ? children : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FinancialsPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading financial terminal...</div>}>
|
||||
@@ -319,6 +456,7 @@ function FinancialsPageContent() {
|
||||
const [ticker, setTicker] = useState('MSFT');
|
||||
const [statement, setStatement] = useState<FinancialStatementKind>('income');
|
||||
const [window, setWindow] = useState<FinancialHistoryWindow>('10y');
|
||||
const [surface, setSurface] = useState<FinancialStatementSurfaceKind>('standardized');
|
||||
const [valueScale, setValueScale] = useState<NumberScaleUnit>('millions');
|
||||
const [financials, setFinancials] = useState<CompanyFinancialStatementsResponse | null>(null);
|
||||
const [overviewIncome, setOverviewIncome] = useState<CompanyFinancialStatementsResponse | null>(null);
|
||||
@@ -412,12 +550,12 @@ function FinancialsPageContent() {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [
|
||||
queryClient,
|
||||
statement,
|
||||
window,
|
||||
dimensionsEnabled,
|
||||
loadOverview,
|
||||
queryClient,
|
||||
selectedRowKey,
|
||||
loadOverview
|
||||
statement,
|
||||
window
|
||||
]);
|
||||
|
||||
const syncFinancials = useCallback(async () => {
|
||||
@@ -440,20 +578,24 @@ function FinancialsPageContent() {
|
||||
} finally {
|
||||
setSyncingFinancials(false);
|
||||
}
|
||||
}, [financials?.company.ticker, ticker, queryClient, loadFinancials]);
|
||||
}, [financials?.company.ticker, loadFinancials, queryClient, ticker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && isAuthenticated) {
|
||||
void loadFinancials(ticker);
|
||||
}
|
||||
}, [isPending, isAuthenticated, ticker, statement, window, dimensionsEnabled, loadFinancials]);
|
||||
}, [isPending, isAuthenticated, loadFinancials, ticker]);
|
||||
|
||||
const periods = useMemo(() => {
|
||||
return [...(financials?.periods ?? [])]
|
||||
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
|
||||
}, [financials?.periods]);
|
||||
|
||||
const statementRows = useMemo(() => financials?.rows ?? [], [financials?.rows]);
|
||||
const faithfulRows = useMemo(() => financials?.surfaces.faithful.rows ?? [], [financials?.surfaces.faithful.rows]);
|
||||
const standardizedRows = useMemo(() => financials?.surfaces.standardized.rows ?? [], [financials?.surfaces.standardized.rows]);
|
||||
const statementRows = useMemo<DisplayRow[]>(() => {
|
||||
return surface === 'standardized' ? standardizedRows : faithfulRows;
|
||||
}, [faithfulRows, standardizedRows, surface]);
|
||||
|
||||
const overviewSeries = useMemo(() => {
|
||||
return buildOverviewSeries(overviewIncome, overviewBalance);
|
||||
@@ -464,11 +606,29 @@ function FinancialsPageContent() {
|
||||
?? overviewIncome?.metrics.taxonomy
|
||||
?? overviewBalance?.metrics.taxonomy
|
||||
?? null;
|
||||
const latestRevenue = latestOverview?.revenue ?? latestTaxonomyMetrics?.revenue ?? null;
|
||||
const latestNetIncome = latestOverview?.netIncome ?? latestTaxonomyMetrics?.netIncome ?? null;
|
||||
const latestTotalAssets = latestOverview?.totalAssets ?? latestTaxonomyMetrics?.totalAssets ?? null;
|
||||
const latestCash = latestOverview?.cash ?? latestTaxonomyMetrics?.cash ?? null;
|
||||
const latestDebt = latestOverview?.debt ?? latestTaxonomyMetrics?.debt ?? null;
|
||||
|
||||
const latestStandardizedIncome = overviewIncome?.surfaces.standardized.rows ?? [];
|
||||
const latestStandardizedBalance = overviewBalance?.surfaces.standardized.rows ?? [];
|
||||
const latestRevenue = latestOverview?.revenue
|
||||
?? findStandardizedRow(latestStandardizedIncome, 'revenue')?.values[periods[periods.length - 1]?.id ?? '']
|
||||
?? latestTaxonomyMetrics?.revenue
|
||||
?? null;
|
||||
const latestNetIncome = latestOverview?.netIncome
|
||||
?? findStandardizedRow(latestStandardizedIncome, 'net-income')?.values[periods[periods.length - 1]?.id ?? '']
|
||||
?? latestTaxonomyMetrics?.netIncome
|
||||
?? null;
|
||||
const latestTotalAssets = latestOverview?.totalAssets
|
||||
?? findStandardizedRow(latestStandardizedBalance, 'total-assets')?.values[periods[periods.length - 1]?.id ?? '']
|
||||
?? latestTaxonomyMetrics?.totalAssets
|
||||
?? null;
|
||||
const latestCash = latestOverview?.cash
|
||||
?? findStandardizedRow(latestStandardizedBalance, 'cash-and-equivalents')?.values[periods[periods.length - 1]?.id ?? '']
|
||||
?? latestTaxonomyMetrics?.cash
|
||||
?? null;
|
||||
const latestDebt = latestOverview?.debt
|
||||
?? findStandardizedRow(latestStandardizedBalance, 'total-debt')?.values[periods[periods.length - 1]?.id ?? '']
|
||||
?? latestTaxonomyMetrics?.debt
|
||||
?? null;
|
||||
const latestReferenceDate = latestOverview?.filingDate ?? periods[periods.length - 1]?.filingDate ?? null;
|
||||
|
||||
const selectedRow = useMemo(() => {
|
||||
@@ -485,20 +645,8 @@ function FinancialsPageContent() {
|
||||
}
|
||||
|
||||
const direct = financials.dimensionBreakdown[selectedRow.key] ?? [];
|
||||
if (direct.length > 0) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const conceptKey = selectedRow.qname.toLowerCase();
|
||||
for (const rows of Object.values(financials.dimensionBreakdown)) {
|
||||
const matched = rows.filter((row) => (row.concept ?? '').toLowerCase() === conceptKey);
|
||||
if (matched.length > 0) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [selectedRow, financials?.dimensionBreakdown]);
|
||||
return groupDimensionRows(direct, surface);
|
||||
}, [financials?.dimensionBreakdown, selectedRow, surface]);
|
||||
|
||||
const selectedScaleLabel = useMemo(() => {
|
||||
return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale)?.label ?? 'Millions (M)';
|
||||
@@ -532,7 +680,7 @@ function FinancialsPageContent() {
|
||||
options: FINANCIAL_VALUE_SCALE_OPTIONS,
|
||||
onChange: (nextValue) => setValueScale(nextValue as NumberScaleUnit)
|
||||
}
|
||||
], [statement, window, valueScale]);
|
||||
], [statement, valueScale, window]);
|
||||
|
||||
const controlActions = useMemo<FinancialControlAction[]>(() => {
|
||||
const actions: FinancialControlAction[] = [];
|
||||
@@ -569,7 +717,7 @@ function FinancialsPageContent() {
|
||||
}
|
||||
|
||||
return actions;
|
||||
}, [window, financials?.nextCursor, loadingMore, loadFinancials, ticker]);
|
||||
}, [financials?.nextCursor, loadFinancials, loadingMore, ticker, window]);
|
||||
|
||||
if (isPending || !isAuthenticated) {
|
||||
return <div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading financial terminal...</div>;
|
||||
@@ -578,7 +726,7 @@ function FinancialsPageContent() {
|
||||
return (
|
||||
<AppShell
|
||||
title="Financials"
|
||||
subtitle="Taxonomy-native financial statements with PDF LLM metric validation."
|
||||
subtitle="Filing-faithful financial statements with a standardized comparison surface and PDF metric validation."
|
||||
activeTicker={financials?.company.ticker ?? ticker}
|
||||
actions={(
|
||||
<div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
|
||||
@@ -604,7 +752,7 @@ function FinancialsPageContent() {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Panel title="Company Selector" subtitle="Load statement history by ticker. Default window is 10 years; full history is on demand.">
|
||||
<Panel title="Company Selector" subtitle="Load statement history by ticker. The standardized comparison view is the default surface.">
|
||||
<form
|
||||
className="flex flex-wrap items-center gap-3"
|
||||
onSubmit={(event) => {
|
||||
@@ -673,28 +821,42 @@ function FinancialsPageContent() {
|
||||
|
||||
<FinancialControlBar
|
||||
title="Financial Controls"
|
||||
subtitle={`Taxonomy statement controls. Current display scale: ${selectedScaleLabel}.`}
|
||||
subtitle={`Current display scale: ${selectedScaleLabel}. Choose between standardized comparison and filing-faithful rendering below.`}
|
||||
sections={controlSections}
|
||||
actions={controlActions}
|
||||
/>
|
||||
|
||||
<Panel title="Statement Surface" subtitle="Standardized rows flatten curated GAAP aliases for comparison while the faithful view preserves the filing's presentation.">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{SURFACE_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={surface === option.value ? 'primary' : 'ghost'}
|
||||
onClick={() => {
|
||||
setSurface(option.value);
|
||||
setSelectedRowKey(null);
|
||||
}}
|
||||
>
|
||||
{option.value === 'standardized' ? <GitCompareArrows className="size-4" /> : <ChartNoAxesCombined className="size-4" />}
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<Panel title="Income Trend" subtitle="Overview chart from taxonomy-derived income and balance concepts.">
|
||||
<Panel title="Income Trend" subtitle="Overview chart sourced from standardized statement rows first, then taxonomy metrics as fallback.">
|
||||
{loading ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Loading overview chart...</p>
|
||||
) : overviewSeries.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No income history available yet.</p>
|
||||
) : (
|
||||
<div className="h-[320px]">
|
||||
<ChartFrame>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={overviewSeries}>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis dataKey="label" stroke={CHART_MUTED} fontSize={12} minTickGap={20} />
|
||||
<YAxis
|
||||
stroke={CHART_MUTED}
|
||||
fontSize={12}
|
||||
tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)}
|
||||
/>
|
||||
<YAxis stroke={CHART_MUTED} fontSize={12} tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)} />
|
||||
<Tooltip
|
||||
formatter={(value) => asTooltipCurrency(value, valueScale)}
|
||||
contentStyle={{
|
||||
@@ -707,26 +869,22 @@ function FinancialsPageContent() {
|
||||
<Bar dataKey="netIncome" name="Net Income" fill="#5fd3ff" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</ChartFrame>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel title="Balance Trend" subtitle="Assets, cash, and debt from taxonomy-derived balance concepts.">
|
||||
<Panel title="Balance Trend" subtitle="Assets, cash, and debt sourced from standardized balance rows for cleaner period-over-period comparison.">
|
||||
{loading ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Loading balance chart...</p>
|
||||
) : overviewSeries.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No balance history available yet.</p>
|
||||
) : (
|
||||
<div className="h-[320px]">
|
||||
<ChartFrame>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={overviewSeries}>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis dataKey="label" stroke={CHART_MUTED} fontSize={12} minTickGap={20} />
|
||||
<YAxis
|
||||
stroke={CHART_MUTED}
|
||||
fontSize={12}
|
||||
tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)}
|
||||
/>
|
||||
<YAxis stroke={CHART_MUTED} fontSize={12} tickFormatter={(value: number) => asAxisCurrencyTick(value, valueScale)} />
|
||||
<Tooltip
|
||||
formatter={(value) => asTooltipCurrency(value, valueScale)}
|
||||
contentStyle={{
|
||||
@@ -740,14 +898,16 @@ function FinancialsPageContent() {
|
||||
<Line type="monotone" dataKey="debt" name="Debt" stroke="#ffd08a" strokeWidth={2} dot={false} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</ChartFrame>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<Panel
|
||||
title="Statement Matrix"
|
||||
subtitle={`Taxonomy-native ${statement.replace('_', ' ')} rows by period. Extension concepts are tagged; rows with dimensions can be drilled down.`}
|
||||
subtitle={surface === 'standardized'
|
||||
? `Standardized ${statement.replace('_', ' ')} rows merge curated GAAP aliases for comparison while preserving source provenance.`
|
||||
: `Filing-faithful ${statement.replace('_', ' ')} rows preserve issuer taxonomy, ordering, extensions, and dimensional detail.`}
|
||||
>
|
||||
{loading ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Loading statement matrix...</p>
|
||||
@@ -782,10 +942,28 @@ function FinancialsPageContent() {
|
||||
}}
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ paddingLeft: `${Math.min(row.depth, 10) * 12}px` }}>{row.label}</span>
|
||||
{row.isExtension ? <span className="rounded border border-[color:var(--line-weak)] px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-[color:var(--terminal-muted)]">Ext</span> : null}
|
||||
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ paddingLeft: `${isFaithfulRow(row) ? Math.min(row.depth, 10) * 12 : 0}px` }}>{row.label}</span>
|
||||
{isFaithfulRow(row) && row.isExtension ? (
|
||||
<span className="rounded border border-[color:var(--line-weak)] px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-[color:var(--terminal-muted)]">Ext</span>
|
||||
) : null}
|
||||
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
</div>
|
||||
{isStandardizedRow(row) ? (
|
||||
<details className="text-xs text-[color:var(--terminal-muted)]">
|
||||
<summary className="cursor-pointer list-none">
|
||||
Mapped from {row.sourceConcepts.length} concept{row.sourceConcepts.length === 1 ? '' : 's'}
|
||||
</summary>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{row.sourceConcepts.map((concept) => (
|
||||
<span key={`${row.key}-${concept}`} className="rounded border border-[color:var(--line-weak)] px-1.5 py-0.5">
|
||||
{concept}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
{periods.map((period) => (
|
||||
@@ -799,10 +977,14 @@ function FinancialsPageContent() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
|
||||
<Panel title="Dimension Drill-down" subtitle="Segment/geography/product axes are shown only for the selected row when available.">
|
||||
<Panel
|
||||
title="Dimension Drill-down"
|
||||
subtitle={surface === 'standardized'
|
||||
? 'Standardized rows aggregate dimensional facts across every mapped source concept.'
|
||||
: 'Filing-faithful rows show the exact dimensional facts attached to the selected concept.'}
|
||||
>
|
||||
{!selectedRow ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Select a statement row to inspect dimensional facts.</p>
|
||||
) : !selectedRow.hasDimensions ? (
|
||||
@@ -813,10 +995,11 @@ function FinancialsPageContent() {
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Dimensions are still loading or unavailable for this row.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<table className="data-table min-w-[860px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
{surface === 'standardized' ? <th>Source</th> : null}
|
||||
<th>Axis</th>
|
||||
<th>Member</th>
|
||||
<th>Value</th>
|
||||
@@ -828,6 +1011,7 @@ function FinancialsPageContent() {
|
||||
return (
|
||||
<tr key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}>
|
||||
<td>{period ? formatLongDate(period.filingDate) : row.periodId}</td>
|
||||
{surface === 'standardized' ? <td>{row.sourceLabel ?? row.concept ?? 'Unknown source'}</td> : null}
|
||||
<td>{row.axis}</td>
|
||||
<td>{row.member}</td>
|
||||
<td>{asDisplayCurrency(row.value, valueScale)}</td>
|
||||
@@ -841,7 +1025,7 @@ function FinancialsPageContent() {
|
||||
</Panel>
|
||||
|
||||
{financials ? (
|
||||
<Panel title="Metric Validation" subtitle="Taxonomy-derived metrics are canonical; PDF LLM extraction is used for validation only.">
|
||||
<Panel title="Metric Validation" subtitle="The standardized taxonomy surface is the canonical financial layer; PDF LLM extraction is used only to validate selected metrics.">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
|
||||
<AlertTriangle className="size-4" />
|
||||
<span>Overall status: {financials.metrics.validation?.status ?? 'not_run'}</span>
|
||||
@@ -878,7 +1062,7 @@ function FinancialsPageContent() {
|
||||
) : null}
|
||||
|
||||
{financials ? (
|
||||
<Panel title="Data Source Status" subtitle="Hydration and taxonomy parsing status for filing snapshots.">
|
||||
<Panel title="Data Source Status" subtitle="Hydration and taxonomy parsing status for filing snapshots powering both financial statement surfaces.">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
|
||||
<p className="text-[color:var(--terminal-muted)]">Hydrated</p>
|
||||
@@ -907,7 +1091,7 @@ function FinancialsPageContent() {
|
||||
<Panel>
|
||||
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
|
||||
<ChartNoAxesCombined className="size-4" />
|
||||
Financial Statements V3: taxonomy + PDF LLM validation
|
||||
Financial Statements V3: faithful filing reconstruction + standardized taxonomy comparison
|
||||
</div>
|
||||
</Panel>
|
||||
</AppShell>
|
||||
|
||||
Reference in New Issue
Block a user