feat(financials): add compact surface UI and graphing states
This commit is contained in:
@@ -22,6 +22,9 @@ import {
|
||||
RefreshCcw,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
import { NormalizationSummary } from '@/components/financials/normalization-summary';
|
||||
import { StatementMatrix } from '@/components/financials/statement-matrix';
|
||||
import { StatementRowInspector } from '@/components/financials/statement-row-inspector';
|
||||
import { AppShell } from '@/components/shell/app-shell';
|
||||
import {
|
||||
FinancialControlBar,
|
||||
@@ -42,10 +45,16 @@ import {
|
||||
type NumberScaleUnit
|
||||
} from '@/lib/format';
|
||||
import { buildGraphingHref } from '@/lib/graphing/catalog';
|
||||
import {
|
||||
buildStatementTree,
|
||||
resolveStatementSelection,
|
||||
type StatementInspectorSelection
|
||||
} from '@/lib/financials/statement-view-model';
|
||||
import { queryKeys } from '@/lib/query/keys';
|
||||
import { companyFinancialStatementsQueryOptions } from '@/lib/query/options';
|
||||
import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
DetailFinancialRow,
|
||||
DerivedFinancialRow,
|
||||
FinancialCadence,
|
||||
FinancialDisplayMode,
|
||||
@@ -54,6 +63,8 @@ import type {
|
||||
RatioRow,
|
||||
StandardizedFinancialRow,
|
||||
StructuredKpiRow,
|
||||
SurfaceDetailMap,
|
||||
SurfaceFinancialRow,
|
||||
TaxonomyStatementRow,
|
||||
TrendSeries
|
||||
} from '@/lib/types';
|
||||
@@ -63,7 +74,8 @@ type LoadOptions = {
|
||||
append?: boolean;
|
||||
};
|
||||
|
||||
type DisplayRow = TaxonomyStatementRow | StandardizedFinancialRow | RatioRow | StructuredKpiRow;
|
||||
type FlatDisplayRow = TaxonomyStatementRow | StandardizedFinancialRow | RatioRow | StructuredKpiRow;
|
||||
type StatementMatrixRow = SurfaceFinancialRow | DetailFinancialRow;
|
||||
|
||||
const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [
|
||||
{ value: 'thousands', label: 'Thousands (K)' },
|
||||
@@ -106,19 +118,23 @@ function formatLongDate(value: string) {
|
||||
return format(parsed, 'MMM dd, yyyy');
|
||||
}
|
||||
|
||||
function isTaxonomyRow(row: DisplayRow): row is TaxonomyStatementRow {
|
||||
function isTaxonomyRow(row: FlatDisplayRow): row is TaxonomyStatementRow {
|
||||
return 'localName' in row;
|
||||
}
|
||||
|
||||
function isDerivedRow(row: DisplayRow): row is DerivedFinancialRow {
|
||||
function isDerivedRow(row: FlatDisplayRow): row is DerivedFinancialRow {
|
||||
return 'formulaKey' in row;
|
||||
}
|
||||
|
||||
function isKpiRow(row: DisplayRow): row is StructuredKpiRow {
|
||||
function isKpiRow(row: FlatDisplayRow): row is StructuredKpiRow {
|
||||
return 'provenanceType' in row;
|
||||
}
|
||||
|
||||
function rowValue(row: DisplayRow, periodId: string) {
|
||||
function rowValue(row: FlatDisplayRow, periodId: string) {
|
||||
return row.values[periodId] ?? null;
|
||||
}
|
||||
|
||||
function statementRowValue(row: StatementMatrixRow, periodId: string) {
|
||||
return row.values[periodId] ?? null;
|
||||
}
|
||||
|
||||
@@ -181,10 +197,10 @@ function chartTickFormatter(
|
||||
}
|
||||
|
||||
function buildDisplayValue(input: {
|
||||
row: DisplayRow;
|
||||
row: FlatDisplayRow;
|
||||
periodId: string;
|
||||
previousPeriodId: string | null;
|
||||
commonSizeRow: DisplayRow | null;
|
||||
commonSizeRow: FlatDisplayRow | null;
|
||||
displayMode: FinancialDisplayMode;
|
||||
showPercentChange: boolean;
|
||||
showCommonSize: boolean;
|
||||
@@ -245,7 +261,78 @@ function buildDisplayValue(input: {
|
||||
});
|
||||
}
|
||||
|
||||
function groupRows(rows: DisplayRow[], categories: CompanyFinancialStatementsResponse['categories']) {
|
||||
function unitFromDetailRow(row: DetailFinancialRow): FinancialUnit {
|
||||
const normalizedUnit = row.unit?.toLowerCase() ?? '';
|
||||
|
||||
if (normalizedUnit.includes('shares')) {
|
||||
return 'shares';
|
||||
}
|
||||
|
||||
if (normalizedUnit === 'pure' || normalizedUnit.includes('ratio')) {
|
||||
return 'ratio';
|
||||
}
|
||||
|
||||
if (normalizedUnit.includes('percent')) {
|
||||
return 'percent';
|
||||
}
|
||||
|
||||
return 'currency';
|
||||
}
|
||||
|
||||
function buildStatementTreeDisplayValue(input: {
|
||||
row: StatementMatrixRow;
|
||||
periodId: string;
|
||||
previousPeriodId: string | null;
|
||||
commonSizeRow: SurfaceFinancialRow | null;
|
||||
showPercentChange: boolean;
|
||||
showCommonSize: boolean;
|
||||
scale: NumberScaleUnit;
|
||||
surfaceKind: Extract<FinancialSurfaceKind, 'income_statement' | 'balance_sheet' | 'cash_flow_statement'>;
|
||||
}) {
|
||||
const current = statementRowValue(input.row, input.periodId);
|
||||
|
||||
if (input.showPercentChange && input.previousPeriodId) {
|
||||
const previous = statementRowValue(input.row, input.previousPeriodId);
|
||||
if (current === null || previous === null || previous === 0) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return formatMetricValue({
|
||||
value: (current - previous) / previous,
|
||||
unit: 'percent',
|
||||
scale: input.scale,
|
||||
rowKey: input.row.key,
|
||||
surfaceKind: input.surfaceKind,
|
||||
isPercentChange: true
|
||||
});
|
||||
}
|
||||
|
||||
if (input.showCommonSize) {
|
||||
const denominator = input.commonSizeRow ? statementRowValue(input.commonSizeRow, input.periodId) : null;
|
||||
if (current === null || denominator === null || denominator === 0) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return formatMetricValue({
|
||||
value: current / denominator,
|
||||
unit: 'percent',
|
||||
scale: input.scale,
|
||||
rowKey: input.row.key,
|
||||
surfaceKind: input.surfaceKind,
|
||||
isCommonSize: true
|
||||
});
|
||||
}
|
||||
|
||||
return formatMetricValue({
|
||||
value: current,
|
||||
unit: 'conceptKey' in input.row ? unitFromDetailRow(input.row) : input.row.unit,
|
||||
scale: input.scale,
|
||||
rowKey: input.row.key,
|
||||
surfaceKind: input.surfaceKind
|
||||
});
|
||||
}
|
||||
|
||||
function groupRows(rows: FlatDisplayRow[], categories: CompanyFinancialStatementsResponse['categories']) {
|
||||
if (categories.length === 0) {
|
||||
return [{ label: null, rows }];
|
||||
}
|
||||
@@ -258,6 +345,42 @@ function groupRows(rows: DisplayRow[], categories: CompanyFinancialStatementsRes
|
||||
.filter((group) => group.rows.length > 0);
|
||||
}
|
||||
|
||||
function mergeDetailMaps(base: SurfaceDetailMap | null, next: SurfaceDetailMap | null) {
|
||||
if (!base) {
|
||||
return next;
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const merged: SurfaceDetailMap = structuredClone(base);
|
||||
|
||||
for (const [surfaceKey, detailRows] of Object.entries(next)) {
|
||||
const existingRows = merged[surfaceKey] ?? [];
|
||||
const rowMap = new Map(existingRows.map((row) => [row.key, row]));
|
||||
|
||||
for (const detailRow of detailRows) {
|
||||
const existing = rowMap.get(detailRow.key);
|
||||
if (!existing) {
|
||||
rowMap.set(detailRow.key, structuredClone(detailRow));
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.values = {
|
||||
...existing.values,
|
||||
...detailRow.values
|
||||
};
|
||||
existing.sourceFactIds = [...new Set([...existing.sourceFactIds, ...detailRow.sourceFactIds])];
|
||||
existing.dimensionsSummary = [...new Set([...existing.dimensionsSummary, ...detailRow.dimensionsSummary])];
|
||||
}
|
||||
|
||||
merged[surfaceKey] = [...rowMap.values()];
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeFinancialPages(
|
||||
base: CompanyFinancialStatementsResponse | null,
|
||||
next: CompanyFinancialStatementsResponse
|
||||
@@ -297,6 +420,7 @@ function mergeFinancialPages(
|
||||
standardized: mergeRows([...base.statementRows.standardized, ...next.statementRows.standardized])
|
||||
}
|
||||
: next.statementRows,
|
||||
statementDetails: mergeDetailMaps(base.statementDetails, next.statementDetails),
|
||||
ratioRows: next.ratioRows && base.ratioRows ? mergeRows([...base.ratioRows, ...next.ratioRows]) : next.ratioRows,
|
||||
kpiRows: next.kpiRows && base.kpiRows ? mergeRows([...base.kpiRows, ...next.kpiRows]) : next.kpiRows,
|
||||
trendSeries: next.trendSeries,
|
||||
@@ -348,7 +472,9 @@ function FinancialsPageContent() {
|
||||
const [showPercentChange, setShowPercentChange] = useState(false);
|
||||
const [showCommonSize, setShowCommonSize] = useState(false);
|
||||
const [financials, setFinancials] = useState<CompanyFinancialStatementsResponse | null>(null);
|
||||
const [selectedRowKey, setSelectedRowKey] = useState<string | null>(null);
|
||||
const [selectedFlatRowKey, setSelectedFlatRowKey] = useState<string | null>(null);
|
||||
const [selectedRowRef, setSelectedRowRef] = useState<StatementInspectorSelection | null>(null);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<Set<string>>(() => new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [syncingFinancials, setSyncingFinancials] = useState(false);
|
||||
@@ -367,6 +493,9 @@ function FinancialsPageContent() {
|
||||
|
||||
setTickerInput(normalized);
|
||||
setTicker(normalized);
|
||||
setSelectedFlatRowKey(null);
|
||||
setSelectedRowRef(null);
|
||||
setExpandedRowKeys(new Set());
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -389,7 +518,8 @@ function FinancialsPageContent() {
|
||||
const loadFinancials = useCallback(async (symbol: string, options?: LoadOptions) => {
|
||||
const normalizedTicker = symbol.trim().toUpperCase();
|
||||
const nextCursor = options?.cursor ?? null;
|
||||
const includeDimensions = selectedRowKey !== null && (surfaceKind === 'segments_kpis' || surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement');
|
||||
const includeDimensions = (selectedFlatRowKey !== null || selectedRowRef !== null)
|
||||
&& (surfaceKind === 'segments_kpis' || surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement');
|
||||
|
||||
if (!options?.append) {
|
||||
setLoading(true);
|
||||
@@ -420,7 +550,7 @@ function FinancialsPageContent() {
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [cadence, queryClient, selectedRowKey, surfaceKind]);
|
||||
}, [cadence, queryClient, selectedFlatRowKey, selectedRowRef, surfaceKind]);
|
||||
|
||||
const syncFinancials = useCallback(async () => {
|
||||
const targetTicker = (financials?.company.ticker ?? ticker).trim().toUpperCase();
|
||||
@@ -455,7 +585,9 @@ function FinancialsPageContent() {
|
||||
.sort((left, right) => Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate));
|
||||
}, [financials?.periods]);
|
||||
|
||||
const activeRows = useMemo<DisplayRow[]>(() => {
|
||||
const isTreeStatementMode = displayMode === 'standardized' && isStatementSurfaceKind(surfaceKind);
|
||||
|
||||
const activeRows = useMemo<FlatDisplayRow[]>(() => {
|
||||
if (!financials) {
|
||||
return [];
|
||||
}
|
||||
@@ -477,35 +609,81 @@ function FinancialsPageContent() {
|
||||
}, [displayMode, financials, surfaceKind]);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
if (isTreeStatementMode) {
|
||||
return activeRows;
|
||||
}
|
||||
|
||||
const normalizedSearch = rowSearch.trim().toLowerCase();
|
||||
if (!normalizedSearch) {
|
||||
return activeRows;
|
||||
}
|
||||
|
||||
return activeRows.filter((row) => row.label.toLowerCase().includes(normalizedSearch));
|
||||
}, [activeRows, rowSearch]);
|
||||
}, [activeRows, isTreeStatementMode, rowSearch]);
|
||||
|
||||
const groupedRows = useMemo(() => {
|
||||
return groupRows(filteredRows, financials?.categories ?? []);
|
||||
}, [filteredRows, financials?.categories]);
|
||||
|
||||
const selectedRow = useMemo(() => {
|
||||
if (!selectedRowKey) {
|
||||
const treeModel = useMemo(() => {
|
||||
if (!isTreeStatementMode || !financials || !isStatementSurfaceKind(surfaceKind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeRows.find((row) => row.key === selectedRowKey) ?? null;
|
||||
}, [activeRows, selectedRowKey]);
|
||||
return buildStatementTree({
|
||||
surfaceKind,
|
||||
rows: financials.statementRows?.standardized ?? [],
|
||||
statementDetails: financials.statementDetails,
|
||||
categories: financials.categories,
|
||||
searchQuery: rowSearch,
|
||||
expandedRowKeys
|
||||
});
|
||||
}, [expandedRowKeys, financials, isTreeStatementMode, rowSearch, surfaceKind]);
|
||||
|
||||
const selectedRow = useMemo(() => {
|
||||
if (!selectedFlatRowKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeRows.find((row) => row.key === selectedFlatRowKey) ?? null;
|
||||
}, [activeRows, selectedFlatRowKey]);
|
||||
|
||||
const selectedStatementRow = useMemo(() => {
|
||||
if (!isTreeStatementMode || !financials || !isStatementSurfaceKind(surfaceKind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveStatementSelection({
|
||||
surfaceKind,
|
||||
rows: financials.statementRows?.standardized ?? [],
|
||||
statementDetails: financials.statementDetails,
|
||||
selection: selectedRowRef
|
||||
});
|
||||
}, [financials, isTreeStatementMode, selectedRowRef, surfaceKind]);
|
||||
|
||||
const dimensionRows = useMemo(() => {
|
||||
if (!selectedRow || !financials?.dimensionBreakdown) {
|
||||
if (!financials?.dimensionBreakdown) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (selectedStatementRow?.kind === 'surface') {
|
||||
return financials.dimensionBreakdown[selectedStatementRow.row.key] ?? [];
|
||||
}
|
||||
|
||||
if (selectedStatementRow?.kind === 'detail') {
|
||||
return financials.dimensionBreakdown[selectedStatementRow.row.key]
|
||||
?? financials.dimensionBreakdown[selectedStatementRow.row.parentSurfaceKey]
|
||||
?? [];
|
||||
}
|
||||
|
||||
if (!selectedRow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return financials.dimensionBreakdown[selectedRow.key] ?? [];
|
||||
}, [financials?.dimensionBreakdown, selectedRow]);
|
||||
}, [financials?.dimensionBreakdown, selectedRow, selectedStatementRow]);
|
||||
|
||||
const commonSizeRow = useMemo<DisplayRow | null>(() => {
|
||||
const commonSizeRow = useMemo<SurfaceFinancialRow | null>(() => {
|
||||
if (displayMode === 'faithful' || !financials?.statementRows) {
|
||||
return null;
|
||||
}
|
||||
@@ -539,7 +717,9 @@ function FinancialsPageContent() {
|
||||
options: SURFACE_OPTIONS,
|
||||
onChange: (value) => {
|
||||
setSurfaceKind(value as FinancialSurfaceKind);
|
||||
setSelectedRowKey(null);
|
||||
setSelectedFlatRowKey(null);
|
||||
setSelectedRowRef(null);
|
||||
setExpandedRowKeys(new Set());
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -549,7 +729,9 @@ function FinancialsPageContent() {
|
||||
options: CADENCE_OPTIONS,
|
||||
onChange: (value) => {
|
||||
setCadence(value as FinancialCadence);
|
||||
setSelectedRowKey(null);
|
||||
setSelectedFlatRowKey(null);
|
||||
setSelectedRowRef(null);
|
||||
setExpandedRowKeys(new Set());
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -562,7 +744,9 @@ function FinancialsPageContent() {
|
||||
options: DISPLAY_MODE_OPTIONS,
|
||||
onChange: (value) => {
|
||||
setDisplayMode(value as FinancialDisplayMode);
|
||||
setSelectedRowKey(null);
|
||||
setSelectedFlatRowKey(null);
|
||||
setSelectedRowRef(null);
|
||||
setExpandedRowKeys(new Set());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -626,6 +810,56 @@ function FinancialsPageContent() {
|
||||
return actions;
|
||||
}, [displayMode, financials?.nextCursor, loadFinancials, loadingMore, selectedRow, showCommonSize, showPercentChange, surfaceKind, ticker]);
|
||||
|
||||
const toggleExpandedRow = useCallback((key: string) => {
|
||||
setExpandedRowKeys((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const rowResultCountLabel = useMemo(() => {
|
||||
if (treeModel) {
|
||||
return `${treeModel.visibleNodeCount} of ${treeModel.totalNodeCount} visible rows`;
|
||||
}
|
||||
|
||||
return `${filteredRows.length} of ${activeRows.length} rows`;
|
||||
}, [activeRows.length, filteredRows.length, treeModel]);
|
||||
|
||||
const valueScaleLabel = FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale)?.label ?? valueScale;
|
||||
|
||||
const renderStatementTreeCellValue = useCallback((row: StatementMatrixRow, periodId: string, previousPeriodId: string | null) => {
|
||||
if (!isStatementSurfaceKind(surfaceKind)) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return buildStatementTreeDisplayValue({
|
||||
row,
|
||||
periodId,
|
||||
previousPeriodId,
|
||||
commonSizeRow,
|
||||
showPercentChange,
|
||||
showCommonSize,
|
||||
scale: valueScale,
|
||||
surfaceKind
|
||||
});
|
||||
}, [commonSizeRow, showCommonSize, showPercentChange, surfaceKind, valueScale]);
|
||||
|
||||
const renderDimensionValue = useCallback((value: number | null, rowKey: string, unit: FinancialUnit) => {
|
||||
return formatMetricValue({
|
||||
value,
|
||||
unit,
|
||||
scale: valueScale,
|
||||
rowKey,
|
||||
surfaceKind
|
||||
});
|
||||
}, [surfaceKind, valueScale]);
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -668,6 +902,9 @@ function FinancialsPageContent() {
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
setSelectedFlatRowKey(null);
|
||||
setSelectedRowRef(null);
|
||||
setExpandedRowKeys(new Set());
|
||||
setTicker(normalized);
|
||||
}}
|
||||
>
|
||||
@@ -726,7 +963,7 @@ function FinancialsPageContent() {
|
||||
className="w-full sm:max-w-sm"
|
||||
/>
|
||||
<span className="text-sm text-[color:var(--terminal-muted)]">
|
||||
{filteredRows.length} of {activeRows.length} rows
|
||||
{rowResultCountLabel}
|
||||
</span>
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -788,6 +1025,10 @@ function FinancialsPageContent() {
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
{financials && isTreeStatementMode ? (
|
||||
<NormalizationSummary normalization={financials.normalization} />
|
||||
) : null}
|
||||
|
||||
<Panel title="Surface Matrix" subtitle="Standardized statements, ratios, and KPIs render in one shared matrix." variant="surface">
|
||||
{loading ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Loading financial matrix...</p>
|
||||
@@ -796,158 +1037,87 @@ function FinancialsPageContent() {
|
||||
<p className="text-sm text-[color:var(--terminal-bright)]">This surface is not yet available in v1.</p>
|
||||
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">Adjusted and custom metrics are API-visible placeholders only. No edits or derived rows are available yet.</p>
|
||||
</div>
|
||||
) : periods.length === 0 || filteredRows.length === 0 ? (
|
||||
) : periods.length === 0 || (isTreeStatementMode ? (treeModel?.visibleNodeCount ?? 0) === 0 : filteredRows.length === 0) ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No rows available for the selected filters yet.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{isStatementSurfaceKind(surfaceKind) ? (
|
||||
<p className="text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
|
||||
USD · {FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === valueScale)?.label ?? valueScale}
|
||||
USD · {valueScaleLabel}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[980px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="sticky left-0 z-10 bg-[color:var(--panel)]">Metric</th>
|
||||
{periods.map((period) => (
|
||||
<th key={period.id}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>{formatLongDate(period.periodEnd ?? period.filingDate)}</span>
|
||||
<span className="text-[11px] normal-case tracking-normal text-[color:var(--terminal-muted)]">{period.filingType} · {period.periodLabel}</span>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedRows.map((group) => (
|
||||
<Fragment key={group.label ?? 'ungrouped'}>
|
||||
{group.label ? (
|
||||
<tr className="bg-[color:var(--panel-soft)]">
|
||||
<td colSpan={periods.length + 1} className="font-semibold text-[color:var(--terminal-bright)]">{group.label}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{group.rows.map((row) => (
|
||||
<tr
|
||||
key={row.key}
|
||||
className={selectedRowKey === row.key ? 'bg-[color:var(--panel-soft)]' : undefined}
|
||||
onClick={() => setSelectedRowKey(row.key)}
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ paddingLeft: `${isTaxonomyRow(row) ? Math.min(row.depth, 10) * 12 : 0}px` }}>{row.label}</span>
|
||||
{'hasDimensions' in row && row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
</div>
|
||||
{isDerivedRow(row) && row.formulaKey ? (
|
||||
<span className="text-xs text-[color:var(--terminal-muted)]">Formula: {row.formulaKey}</span>
|
||||
) : null}
|
||||
{isKpiRow(row) ? (
|
||||
<span className="text-xs text-[color:var(--terminal-muted)]">Provenance: {row.provenanceType}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
{periods.map((period, index) => (
|
||||
<td key={`${row.key}-${period.id}`}>
|
||||
{buildDisplayValue({
|
||||
row,
|
||||
periodId: period.id,
|
||||
previousPeriodId: index > 0 ? periods[index - 1]?.id ?? null : null,
|
||||
commonSizeRow,
|
||||
displayMode,
|
||||
showPercentChange,
|
||||
showCommonSize,
|
||||
scale: valueScale,
|
||||
surfaceKind
|
||||
})}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel title="Row Details" subtitle="Inspect provenance, formulas, and dimensional evidence for the selected row." variant="surface">
|
||||
{!selectedRow ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Select a row to inspect details.</p>
|
||||
) : (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Label</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.label}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Key</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.key}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTaxonomyRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Taxonomy Concept</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.qname}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Category</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.category}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Unit</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDerivedRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Source Row Keys</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceRowKeys.join(', ') || 'n/a'}</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Concepts</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceConcepts.join(', ') || 'n/a'}</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Fact IDs</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceFactIds.join(', ') || 'n/a'}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!selectedRow || !('hasDimensions' in selectedRow) || !selectedRow.hasDimensions ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional drill-down is available for this row.</p>
|
||||
) : dimensionRows.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional facts were returned for the selected row.</p>
|
||||
{isTreeStatementMode && treeModel ? (
|
||||
<StatementMatrix
|
||||
periods={periods}
|
||||
sections={treeModel.sections}
|
||||
selectedRowRef={selectedRowRef}
|
||||
onToggleRow={toggleExpandedRow}
|
||||
onSelectRow={setSelectedRowRef}
|
||||
renderCellValue={renderStatementTreeCellValue}
|
||||
periodLabelFormatter={formatLongDate}
|
||||
/>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<table className="data-table min-w-[980px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Axis</th>
|
||||
<th>Member</th>
|
||||
<th>Value</th>
|
||||
<th className="sticky left-0 z-10 bg-[color:var(--panel)]">Metric</th>
|
||||
{periods.map((period) => (
|
||||
<th key={period.id}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>{formatLongDate(period.periodEnd ?? period.filingDate)}</span>
|
||||
<span className="text-[11px] normal-case tracking-normal text-[color:var(--terminal-muted)]">{period.filingType} · {period.periodLabel}</span>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dimensionRows.map((row, index) => (
|
||||
<tr key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}>
|
||||
<td>{periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}</td>
|
||||
<td>{row.axis}</td>
|
||||
<td>{row.member}</td>
|
||||
<td>{formatMetricValue({
|
||||
value: row.value,
|
||||
unit: isTaxonomyRow(selectedRow) ? 'currency' : selectedRow.unit,
|
||||
scale: valueScale,
|
||||
rowKey: selectedRow.key,
|
||||
surfaceKind
|
||||
})}</td>
|
||||
</tr>
|
||||
{groupedRows.map((group) => (
|
||||
<Fragment key={group.label ?? 'ungrouped'}>
|
||||
{group.label ? (
|
||||
<tr className="bg-[color:var(--panel-soft)]">
|
||||
<td colSpan={periods.length + 1} className="font-semibold text-[color:var(--terminal-bright)]">{group.label}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{group.rows.map((row) => (
|
||||
<tr
|
||||
key={row.key}
|
||||
className={selectedFlatRowKey === row.key ? 'bg-[color:var(--panel-soft)]' : undefined}
|
||||
onClick={() => setSelectedFlatRowKey(row.key)}
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ paddingLeft: `${isTaxonomyRow(row) ? Math.min(row.depth, 10) * 12 : 0}px` }}>{row.label}</span>
|
||||
{'hasDimensions' in row && row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
|
||||
</div>
|
||||
{isDerivedRow(row) && row.formulaKey ? (
|
||||
<span className="text-xs text-[color:var(--terminal-muted)]">Formula: {row.formulaKey}</span>
|
||||
) : null}
|
||||
{isKpiRow(row) ? (
|
||||
<span className="text-xs text-[color:var(--terminal-muted)]">Provenance: {row.provenanceType}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
{periods.map((period, index) => (
|
||||
<td key={`${row.key}-${period.id}`}>
|
||||
{buildDisplayValue({
|
||||
row,
|
||||
periodId: period.id,
|
||||
previousPeriodId: index > 0 ? periods[index - 1]?.id ?? null : null,
|
||||
commonSizeRow,
|
||||
displayMode,
|
||||
showPercentChange,
|
||||
showCommonSize,
|
||||
scale: valueScale,
|
||||
surfaceKind
|
||||
})}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -957,6 +1127,100 @@ function FinancialsPageContent() {
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
{isTreeStatementMode && isStatementSurfaceKind(surfaceKind) ? (
|
||||
<StatementRowInspector
|
||||
selection={selectedStatementRow}
|
||||
dimensionRows={dimensionRows}
|
||||
periods={periods}
|
||||
surfaceKind={surfaceKind}
|
||||
renderValue={renderStatementTreeCellValue}
|
||||
renderDimensionValue={renderDimensionValue}
|
||||
/>
|
||||
) : (
|
||||
<Panel title="Row Details" subtitle="Inspect provenance, formulas, and dimensional evidence for the selected row." variant="surface">
|
||||
{!selectedRow ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">Select a row to inspect details.</p>
|
||||
) : (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Label</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.label}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Key</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.key}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTaxonomyRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Taxonomy Concept</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.qname}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Category</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.category}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Unit</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDerivedRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<p className="text-[color:var(--terminal-muted)]">Source Row Keys</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceRowKeys.join(', ') || 'n/a'}</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Concepts</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceConcepts.join(', ') || 'n/a'}</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">Source Fact IDs</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">{selectedRow.sourceFactIds.join(', ') || 'n/a'}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!selectedRow || !('hasDimensions' in selectedRow) || !selectedRow.hasDimensions ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional drill-down is available for this row.</p>
|
||||
) : dimensionRows.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">No dimensional facts were returned for the selected row.</p>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Axis</th>
|
||||
<th>Member</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dimensionRows.map((row, index) => (
|
||||
<tr key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}>
|
||||
<td>{periods.find((period) => period.id === row.periodId)?.periodLabel ?? row.periodId}</td>
|
||||
<td>{row.axis}</td>
|
||||
<td>{row.member}</td>
|
||||
<td>{formatMetricValue({
|
||||
value: row.value,
|
||||
unit: isTaxonomyRow(selectedRow) ? 'currency' : selectedRow.unit,
|
||||
scale: valueScale,
|
||||
rowKey: selectedRow.key,
|
||||
surfaceKind
|
||||
})}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{(surfaceKind === 'income_statement' || surfaceKind === 'balance_sheet' || surfaceKind === 'cash_flow_statement') && financials ? (
|
||||
<Panel title="Metric Validation" subtitle="Validation remains limited to statement-derived taxonomy metrics in v1." variant="surface">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
|
||||
|
||||
Reference in New Issue
Block a user