export const DASHBOARD_LINE_TOP_N = 10; export const DASHBOARD_SERIES_NONE = 'none'; export const DASHBOARD_SERIES_OTHER = 'other'; export const DASHBOARD_SERIES_ALL = 'all'; export type DashboardSeriesPoint = { seriesId: string; period: string; value: number; }; export type DashboardNamedSeries = { id: string; name: string; values: number[]; }; export function normalizeSeriesId(raw: string | number | bigint | null | undefined): string { if (raw == null) return DASHBOARD_SERIES_NONE; const s = String(raw).trim(); return s ? s : DASHBOARD_SERIES_NONE; } export function periodValueMap( periodKeys: string[], points: DashboardSeriesPoint[], ): Map { const index = new Map(periodKeys.map((k, i) => [k, i])); const map = new Map(); const ensure = (id: string) => { let arr = map.get(id); if (!arr) { arr = periodKeys.map(() => 0); map.set(id, arr); } return arr; }; for (const p of points) { const i = index.get(p.period); if (i === undefined) continue; ensure(p.seriesId)[i] += p.value; } return map; } export function cumulativeValues(increments: number[], baseline = 0): number[] { let acc = baseline; return increments.map((n) => { acc += n; return acc; }); } export function seriesEndWeight(increments: number[], baseline = 0): number { return baseline + increments.reduce((sum, n) => sum + n, 0); } export function pickTopSeriesIds( weights: Array<{ id: string; weight: number }>, topN = DASHBOARD_LINE_TOP_N, reserved: string[] = [DASHBOARD_SERIES_NONE], ): { keep: string[]; rest: string[] } { const reservedSet = new Set(reserved); const reservedIds = weights .filter((w) => reservedSet.has(w.id) && w.weight !== 0) .map((w) => w.id); const ranked = weights .filter((w) => !reservedSet.has(w.id)) .sort((a, b) => b.weight - a.weight || a.id.localeCompare(b.id)); return { keep: [...reservedIds, ...ranked.slice(0, topN).map((w) => w.id)], rest: ranked.slice(topN).map((w) => w.id), }; } function sumSeries( incrementMap: Map, baselineMap: Map, ids: string[], periodLen: number, ): { increments: number[]; baseline: number } { const increments = Array.from({ length: periodLen }, () => 0); let baseline = 0; for (const id of ids) { const arr = incrementMap.get(id); if (arr) { for (let i = 0; i < periodLen; i += 1) increments[i] += arr[i] ?? 0; } baseline += baselineMap.get(id) ?? 0; } return { increments, baseline }; } function applyRound(values: number[], round?: (n: number) => number): number[] { return round ? values.map(round) : values; } export function buildDimensionLines(opts: { periodKeys: string[]; points: DashboardSeriesPoint[]; baselines?: Array<{ seriesId: string; value: number }>; names?: Map; noneLabel: string; otherLabel?: string; topN?: number; round?: (n: number) => number; }): { total: DashboardNamedSeries[]; increment: DashboardNamedSeries[] } { const periodLen = opts.periodKeys.length; const incrementMap = periodValueMap(opts.periodKeys, opts.points); const baselineMap = new Map(); for (const row of opts.baselines ?? []) { const id = normalizeSeriesId(row.seriesId); baselineMap.set(id, (baselineMap.get(id) ?? 0) + row.value); if (!incrementMap.has(id)) incrementMap.set(id, opts.periodKeys.map(() => 0)); } for (const id of incrementMap.keys()) { if (!baselineMap.has(id)) baselineMap.set(id, 0); } const weights = [...incrementMap.keys()].map((id) => ({ id, weight: seriesEndWeight(incrementMap.get(id) ?? [], baselineMap.get(id) ?? 0), })); const { keep, rest } = pickTopSeriesIds(weights, opts.topN ?? DASHBOARD_LINE_TOP_N); const nameOf = (id: string) => { if (id === DASHBOARD_SERIES_NONE) return opts.noneLabel; if (id === DASHBOARD_SERIES_OTHER) return opts.otherLabel ?? '其他'; return opts.names?.get(id) || `#${id}`; }; const orderedIds = [...keep]; if (rest.length) orderedIds.push(DASHBOARD_SERIES_OTHER); const total: DashboardNamedSeries[] = []; const increment: DashboardNamedSeries[] = []; for (const id of orderedIds) { const packed = id === DASHBOARD_SERIES_OTHER ? sumSeries(incrementMap, baselineMap, rest, periodLen) : { increments: incrementMap.get(id) ?? opts.periodKeys.map(() => 0), baseline: baselineMap.get(id) ?? 0, }; increment.push({ id, name: nameOf(id), values: applyRound(packed.increments, opts.round), }); total.push({ id, name: nameOf(id), values: applyRound(cumulativeValues(packed.increments, packed.baseline), opts.round), }); } return { total, increment }; } export function buildSingleLine(opts: { periodKeys: string[]; points: DashboardSeriesPoint[]; baseline?: number; name: string; round?: (n: number) => number; }): { total: DashboardNamedSeries; increment: DashboardNamedSeries } { const merged: DashboardSeriesPoint[] = opts.points.map((p) => ({ ...p, seriesId: DASHBOARD_SERIES_ALL, })); const incrementMap = periodValueMap(opts.periodKeys, merged); const increments = incrementMap.get(DASHBOARD_SERIES_ALL) ?? opts.periodKeys.map(() => 0); const baseline = opts.baseline ?? 0; return { increment: { id: DASHBOARD_SERIES_ALL, name: opts.name, values: applyRound(increments, opts.round), }, total: { id: DASHBOARD_SERIES_ALL, name: opts.name, values: applyRound(cumulativeValues(increments, baseline), opts.round), }, }; }