// Pure binning of a set of matched/in-scope slugs into chart-ready series, // shared by the search-page chart view (bins the live `treeProgress` results) // and the search-derived dashboard hook (`useSearchSeries`). It reuses the same // x-axis labelling and series grouping as metadata charts (chartAggregate.ts) // so search and metadata charts look identical. // // Callers MUST pre-scope `slugs` (e.g. to filter-passing, stat-present videos); // this function does NOT re-apply chart filters — re-filtering would double- // filter and silently drop slugs absent from the stats dataset. import type { VideoStat } from "./stats"; import type { ChartConfig } from "./chartConfig"; import { xLabelsForStat, seriesKeyForStat, type ChartData, type ChartSeries, } from "./chartAggregate"; export function seriesFromSlugs( slugs: Iterable, statBySlug: ReadonlyMap, shape: Pick, weightForSlug: (slug: string) => number, ): ChartData { // seriesKey -> xLabel -> { value, sort } const buckets = new Map< string, Map >(); for (const slug of slugs) { const stat = statBySlug.get(slug); if (!stat) continue; const weight = weightForSlug(slug); const sKey = seriesKeyForStat(stat, shape.groupBy); let xMap = buckets.get(sKey); if (!xMap) { xMap = new Map(); buckets.set(sKey, xMap); } for (const { label, sort } of xLabelsForStat(stat, shape.x)) { const cell = xMap.get(label); if (cell) cell.value += weight; else xMap.set(label, { value: weight, sort }); } } const sortByLabel = new Map(); for (const xMap of buckets.values()) for (const [label, cell] of xMap) sortByLabel.set(label, cell.sort); const categories = Array.from(sortByLabel.keys()); if (shape.x.kind === "category") { const total = (label: string) => { let t = 0; for (const xMap of buckets.values()) t += xMap.get(label)?.value ?? 0; return t; }; categories.sort((a, b) => total(b) - total(a)); } else { categories.sort((a, b) => { const sa = sortByLabel.get(a)!; const sb = sortByLabel.get(b)!; return typeof sa === "number" && typeof sb === "number" ? sa - sb : String(sa).localeCompare(String(sb)); }); } const series: ChartSeries[] = []; for (const [sKey, xMap] of buckets) { let running = 0; const points = categories.map((label) => { const v = xMap.get(label)?.value ?? 0; if (shape.cumulative) { running += v; return { x: label, y: running }; } return { x: label, y: v }; }); series.push({ key: sKey, points }); } series.sort( (a, b) => b.points.reduce((s, p) => s + p.y, 0) - a.points.reduce((s, p) => s + p.y, 0), ); return { categories, series }; }