type GroupSort = 'count-desc' | 'count-asc' | 'label'; export function dimensionCountUnit(dimension: string): string { if (dimension === 'customer') return '家'; if (dimension === 'manager') return '位'; if (dimension === 'date') return '天'; if (['type', 'model', 'assetStatus', 'assetDisposition', 'operationKind', 'inventoryState', 'abnormalType', 'contract_type'].includes(dimension)) return '种'; if (dimension === 'abnormalAge') return '组'; return '个'; } /** Distinct immediate children within each full parent prefix, across all filtered results. */ export function dimensionChildCounts(rows: readonly T[], dimensionCount: number, values: (row: T) => readonly string[]): number[][] { const children = new Map>(); const keys = rows.map(row => { const parts = values(row); return Array.from({ length: Math.max(0, dimensionCount - 1) }, (_, depth) => { const key = JSON.stringify(parts.slice(0, depth + 1)); const set = children.get(key) ?? new Set(); set.add(parts[depth + 1]); children.set(key, set); return key; }); }); return keys.map(row => row.map(key => children.get(key)!.size)); } /** Use all filtered groups before rendering a prefix so each parent includes every descendant. */ export function dimensionParentTotals( rows: readonly T[], dimensionCount: number, values: (row: T) => readonly string[], count: (row: T) => number, ): number[][] { const keys = rows.map(row => Array.from({ length: Math.max(0, dimensionCount - 1) }, (_, depth) => JSON.stringify(values(row).slice(0, depth + 1)))); const totals = new Map(); rows.forEach((row, index) => { for (const key of keys[index]) totals.set(key, (totals.get(key) || 0) + count(row)); }); return keys.map(row => row.map(key => totals.get(key) || 0)); } /** Sort siblings by their combined count, keeping every parent subtree contiguous. */ export function orderDimensionGroups( rows: readonly T[], dimensionCount: number, values: (row: T) => readonly string[], count: (row: T) => number, sort: GroupSort = 'count-desc', compareAt?: (a: T, b: T, index: number) => number, ): T[] { const visit = (siblings: readonly T[], depth: number): T[] => { if (depth >= dimensionCount || siblings.length < 2) return [...siblings]; const groups = new Map(); for (const row of siblings) { const key = values(row)[depth]; const group = groups.get(key) ?? { rows: [], count: 0 }; group.rows.push(row); group.count += count(row); groups.set(key, group); } return [...groups.values()].sort((a, b) => { const difference = sort === 'count-desc' ? b.count - a.count : sort === 'count-asc' ? a.count - b.count : 0; return difference || (compareAt ? compareAt(a.rows[0], b.rows[0], depth) : values(a.rows[0])[depth].localeCompare(values(b.rows[0])[depth], 'zh-CN', { numeric: true })); }).flatMap(group => visit(group.rows, depth + 1)); }; return visit(rows, 0); } /** A child is merged only within its full parent prefix. */ export function dimensionRowSpans( rows: readonly T[], dimensionCount: number, values: (row: T) => readonly string[], ): number[][] { const keys = rows.map(values); const spans = rows.map(() => Array(dimensionCount).fill(1)); for (let dimension = 0; dimension < dimensionCount - 1; dimension += 1) { let start = 0; while (start < rows.length) { let end = start + 1; while (end < rows.length && keys[start].slice(0, dimension + 1).every((value, index) => value === keys[end][index])) end += 1; spans[start][dimension] = end - start; for (let row = start + 1; row < end; row += 1) spans[row][dimension] = 0; start = end; } } return spans; } /** Grow parent cells with the rendered prefix while reusing the complete span calculation. */ export function visibleDimensionRowSpans(spans: readonly (readonly number[])[], visibleCount: number): number[][] { const count = Math.min(spans.length, Math.max(0, Math.floor(visibleCount))); return spans.slice(0, count).map((row, index) => row.map(span => Math.min(span, count - index))); }