@@ -0,0 +1,184 @@
|
||||
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),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user