Files
dukang/packages/domain/src/dashboard-series.ts
T
jacy 68a4b7f984
CI / verify (pull_request) Waiting to run
v4.0.14概览和日报
2026-09-02 21:11:58 +08:00

185 lines
5.6 KiB
TypeScript

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<string, number[]> {
const index = new Map(periodKeys.map((k, i) => [k, i]));
const map = new Map<string, number[]>();
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<string, number[]>,
baselineMap: Map<string, number>,
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<string, string>;
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<string, number>();
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),
},
};
}