diff --git a/components/leaderboard-view.tsx b/components/leaderboard-view.tsx index 0b34da2..1451dff 100644 --- a/components/leaderboard-view.tsx +++ b/components/leaderboard-view.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams, useRouter, usePathname } from 'next/navigation' import type { LeaderboardEntry, BenchmarkVersion } from '@/lib/types' import { PROVIDER_COLORS } from '@/lib/types' @@ -52,6 +52,8 @@ export function LeaderboardView({ entries, lastUpdated, versions, currentVersion const [providerFilter, setProviderFilterState] = useState(initialProvider) const [openWeightsOnly, setOpenWeightsOnlyState] = useState(initialOpenWeights) const [graphSubTab, setGraphSubTabState] = useState(initialGraphTab) + // Hidden providers — managed here so Header counts stay in sync with graph legend toggles + const [hiddenProviders, setHiddenProviders] = useState>(new Set()) // Helper to update URL params without full page reload const updateUrl = useCallback((updates: Record) => { @@ -100,31 +102,57 @@ export function LeaderboardView({ entries, lastUpdated, versions, currentVersion updateUrl({ official: v ? null : 'false' }) }, [updateUrl]) - const filteredEntries = useMemo(() => { - let result = entries - if (providerFilter) { - result = result.filter( - (entry) => entry.provider.toLowerCase() === providerFilter.toLowerCase() - ) - } - if (openWeightsOnly) { - result = result.filter((entry) => entry.weights === 'Open') - } - return result + // Business-level filters only (provider filter, open weights). + // Used for legend provider list and all charts/tables. + const businessFilteredEntries = useMemo(() => { + return entries.filter(entry => { + if (providerFilter && entry.provider.toLowerCase() !== providerFilter.toLowerCase()) return false + if (openWeightsOnly && entry.weights !== 'Open') return false + return true + }) }, [entries, providerFilter, openWeightsOnly]) + // Scatter-visible entries: business filters + legend-hidden providers. + // Used only for scatter graph: chart dots and header totalRuns. + const scatterVisibleEntries = useMemo(() => { + return businessFilteredEntries.filter(entry => + !hiddenProviders.has(entry.provider.toLowerCase()) + ) + }, [businessFilteredEntries, hiddenProviders]) + + // When business filters change, prune hiddenProviders to remove providers + // that are no longer present (e.g. after changing provider filter or open-weights). + const prevBusinessFiltersRef = useRef({ providerFilter, openWeightsOnly }) + useEffect(() => { + const prev = prevBusinessFiltersRef.current + if (prev.providerFilter !== providerFilter || prev.openWeightsOnly !== openWeightsOnly) { + prevBusinessFiltersRef.current = { providerFilter, openWeightsOnly } + const currentProviders = new Set(businessFilteredEntries.map(e => e.provider.toLowerCase())) + setHiddenProviders(prev => { + const pruned = new Set([...prev].filter(k => currentProviders.has(k))) + return pruned.size === prev.size ? prev : pruned + }) + } + }, [providerFilter, openWeightsOnly, businessFilteredEntries]) + const providerColor = providerFilter ? PROVIDER_COLORS[providerFilter.toLowerCase()] || '#666' : undefined + // Which entries to show in the Header stats: + // - In scatter graph view: use scatterVisibleEntries (excludes legend-hidden providers) + // - Otherwise: use businessFilteredEntries (legend-hidden providers don't affect totals) + const headerEntries = + view === 'graphs' && graphSubTab === 'scatter' ? scatterVisibleEntries : businessFilteredEntries + const totalRuns = useMemo(() => { - return filteredEntries.reduce((sum, entry) => sum + (entry.submission_count ?? 0), 0) - }, [filteredEntries]) + return headerEntries.reduce((sum, entry) => sum + (entry.submission_count ?? 0), 0) + }, [headerEntries]) return (
{graphSubTab === 'scatter' && ( - + )} {graphSubTab === 'heatmap' && ( - + )} {graphSubTab === 'distribution' && ( - + )} {graphSubTab === 'radar' && ( - + )}
) : (