"use client"; import { useMemo } from "react"; import { useStats } from "../statsCache"; import type { VideoStat } from "../../lib/stats"; import { aggregate, type ChartData } from "../../lib/chartAggregate"; import { seriesFromSlugs } from "../../lib/chartSeriesFromSlugs"; import type { ChartConfig } from "../../lib/chartConfig"; import type { ChartShape } from "../../lib/chartShare"; import { ChartView } from "./ChartView"; const EMPTY: ChartData = { categories: [], series: [] }; // Renders the single search-page chart. Loads the stats dataset lazily (only // mounted when the chart view is active), then either bins the live search // results (`slugs`/`hitsBySlug` from the page's treeProgress) or aggregates // metadata over the in-scope videos. Reuses ChartView unchanged. export function SearchChartPanel({ slugs, hitsBySlug, scopeSlugs, shape, loading, }: { // Search mode: matched slugs (already filter-scoped). Browse mode: in-scope slugs. slugs: Iterable; // Per-slug hit lists from treeProgress; null when there's no active query. hitsBySlug: ReadonlyMap> | null; // In-scope slugs for metadata aggregation (respects committed filters). scopeSlugs: readonly string[]; shape: ChartShape; loading: boolean; }) { const { stats, ready, loadedPages, pageCount, error } = useStats(); const statBySlug = useMemo(() => { const m = new Map(); for (const s of stats) m.set(s.slug, s); return m; }, [stats]); // ChartView wants a full ChartConfig; build a throwaway one from the shape. // The `search` block is only used by yAxisLabel to title the axis. const config = useMemo( () => ({ id: "live-chart", title: "", type: shape.type, x: shape.x, y: shape.y, groupBy: shape.groupBy, cumulative: shape.cumulative, filters: {}, search: shape.dataSource === "search" ? { queryTree: "", metric: shape.metric } : undefined, }), [shape], ); const { data, charted, hits } = useMemo(() => { if (!ready) return { data: EMPTY, charted: 0, hits: 0 }; if (shape.dataSource === "metadata") { const scope = new Set(scopeSlugs); const scoped = stats.filter((s) => scope.has(s.slug)); return { data: aggregate(scoped, config), charted: scoped.length, hits: 0 }; } let chartedCount = 0; let hitTotal = 0; const eligible: string[] = []; const hitCount = (slug: string) => hitsBySlug?.get(slug)?.length ?? 0; for (const slug of slugs) { if (!statBySlug.has(slug)) continue; chartedCount += 1; hitTotal += hitCount(slug); eligible.push(slug); } const series = seriesFromSlugs(eligible, statBySlug, shape, (slug) => shape.metric === "totalHits" ? hitCount(slug) : 1, ); return { data: series, charted: chartedCount, hits: hitTotal }; }, [ready, shape, stats, statBySlug, slugs, hitsBySlug, scopeSlugs, config]); if (error) { return (

Failed to load the stats dataset ({error.message}). It may not have been built yet — run the stats build.

); } if (!ready) { return (

Loading stats…{" "} {pageCount > 0 ? `(${loadedPages}/${pageCount} pages)` : ""}

); } return (

{charted} {charted === 1 ? "video" : "videos"} {shape.dataSource === "search" ? ` · ${hits} hits` : ""} {loading ? " · searching…" : ""}

); }