From c64a473adc30653ff423a5a0cbb0e4ba491590dc Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sat, 25 Jul 2026 17:55:09 +0200 Subject: [PATCH] cross-dimension tab filtering: hide venues with no data for selected chain --- src/app/benchmarks/[slug]/page.tsx | 1 + src/components/benchmark-body.tsx | 47 +++++++++++++++++++++++++----- src/lib/materialize/load.ts | 18 ++++++++++-- src/lib/snapshot.ts | 1 + src/types/benchmark.ts | 4 +++ 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index f502e8fa0..966eda10d 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -732,6 +732,7 @@ export default async function BenchmarkPage({ regionOptions={regionOptions} kindOptions={kindOptions} venueOptions={venueOptions} + venuesForChain={aggregate.extras?.venuesForChain} initialChain={chain ?? null} initialRegion={region ?? null} initialKind={kind ?? null} diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index c4d876e65..d620b135b 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -142,6 +142,7 @@ export function BenchmarkBody({ regionOptions, kindOptions = [], venueOptions = [], + venuesForChain, initialChain, initialRegion, initialKind = null, @@ -153,6 +154,9 @@ export function BenchmarkBody({ regionOptions: ChainOption[]; kindOptions?: ChainOption[]; venueOptions?: ChainOption[]; + /** Per-chain venue availability map. When present, venue tabs are filtered + * to only show venues that have data for the currently selected chain. */ + venuesForChain?: Record; initialChain: string | null; initialRegion: string | null; initialKind?: string | null; @@ -218,6 +222,33 @@ export function BenchmarkBody({ const effectiveKind = kindOptions.length > 0 ? (kind ?? fallbackKind) : null; const effectiveVenue = venueOptions.length > 0 ? (venue ?? fallbackVenue) : null; + // Cross-dimension filtering: hide venue tabs with no data for the active + // chain, and hide chain tabs with no data for the active venue. + const filteredVenueOptions = useMemo(() => { + if (!venuesForChain || !effectiveChain || effectiveChain === "all") return venueOptions; + const valid = venuesForChain[effectiveChain]; + if (!valid || valid.length === 0) return venueOptions; + const validSet = new Set(valid); + return venueOptions.filter((v) => v.value === "all" || validSet.has(v.value)); + }, [venueOptions, venuesForChain, effectiveChain]); + + const chainsForVenue = useMemo(() => { + if (!venuesForChain) return undefined; + const out: Record = {}; + for (const [c, venues] of Object.entries(venuesForChain)) { + for (const v of venues) (out[v] ??= []).push(c); + } + return out; + }, [venuesForChain]); + + const filteredChainOptions = useMemo(() => { + if (!chainsForVenue || !effectiveVenue || effectiveVenue === "all") return chainOptions; + const valid = chainsForVenue[effectiveVenue]; + if (!valid || valid.length === 0) return chainOptions; + const validSet = new Set(valid); + return chainOptions.filter((c) => c.value === "all" || validSet.has(c.value)); + }, [chainOptions, chainsForVenue, effectiveVenue]); + // The page ships ONLY the aggregate view (embedding every variant made // ISR regenerations take 30-60 s). Filtered variants are fetched here // on demand; while one loads, the aggregate keeps rendering so the tab @@ -544,10 +575,10 @@ export function BenchmarkBody({ return ( <> {(hasLayerSplit || - chainOptions.length > 0 || + filteredChainOptions.length > 0 || regionOptions.length > 0 || kindOptions.length > 0 || - venueOptions.length > 0) && ( + filteredVenueOptions.length > 0) && (
{hasLayerSplit && ( setLayer(v as ProviderLayer)} /> )} - {venueOptions.length > 0 && ( + {filteredVenueOptions.length > 0 && ( [ o.value, summarize( @@ -596,14 +627,14 @@ export function BenchmarkBody({ )} /> )} - {chainOptions.length > 0 && ( + {filteredChainOptions.length > 0 && ( [ o.value, summarize(variantMap[variantKey(o.value, effectiveRegion, effectiveKind, effectiveVenue)]), diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index 053959656..4aeb6b4d0 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -313,6 +313,9 @@ export async function specToBenchmark( if (cellRankResult?.venuesWithData?.length) { live.extras.venuesWithData = cellRankResult.venuesWithData; } + if (cellRankResult?.venuesForChain && Object.keys(cellRankResult.venuesForChain).length > 0) { + live.extras.venuesForChain = cellRankResult.venuesForChain; + } // Per-provider sample-health classification. When the spec declares // expected_n, every live provider gets `dataConfidence` (healthy / @@ -441,6 +444,7 @@ export function propagateNullsToCoarser( type CellRankResult = { ranks: Record; venuesWithData: string[]; + venuesForChain: Record; }; async function tryLoadCellRanks( @@ -466,6 +470,7 @@ async function tryLoadCellRanks( .map((v) => [v.value.toLowerCase(), v.value] as const), ); const venuesWithDataSet = new Set(); + const venuesForChainMap = new Map>(); const chainByLower = new Map( (spec.dimensions?.chain ?? []) .filter((c) => c.value !== "all") @@ -497,7 +502,14 @@ async function tryLoadCellRanks( if (!Number.isFinite(v) || v <= 0) continue; if (venueByLower.size > 0 && sample.metric.venue) { const venue = venueByLower.get(sample.metric.venue.toLowerCase()); - if (venue) venuesWithDataSet.add(venue); + if (venue) { + venuesWithDataSet.add(venue); + if (chain) { + const set = venuesForChainMap.get(chain) ?? new Set(); + set.add(venue); + venuesForChainMap.set(chain, set); + } + } } const key = `${chain ?? "all"}|${region ?? "all"}`; const cell = acc.get(key) ?? new Map(); @@ -571,7 +583,9 @@ async function tryLoadCellRanks( (region) => `all|${region}`, ); } - return { ranks: out, venuesWithData: [...venuesWithDataSet] }; + const venuesForChain: Record = {}; + for (const [c, set] of venuesForChainMap) venuesForChain[c] = [...set]; + return { ranks: out, venuesWithData: [...venuesWithDataSet], venuesForChain }; } catch (e) { console.warn( `cellRanks skip: ${spec.slug} matrix query failed: ${e instanceof Error ? e.message : String(e)}`, diff --git a/src/lib/snapshot.ts b/src/lib/snapshot.ts index e72d1ff09..1bcab0d17 100644 --- a/src/lib/snapshot.ts +++ b/src/lib/snapshot.ts @@ -113,6 +113,7 @@ const ResultExtrasSchema = z.object({ .optional(), regions: z.record(z.string(), z.array(RegionPointSchema)), venuesWithData: z.array(z.string()).optional(), + venuesForChain: z.record(z.string(), z.array(z.string())).optional(), }); const MetricPanelSchema = z.object({ diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index 6fa397464..ac5ed0194 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -171,6 +171,10 @@ export type ResultExtras = { regions: Record; /** Venue dimension values that have actual Prom data in the current window. */ venuesWithData?: string[]; + /** Per-chain venue availability: chain value → venue values that have Prom + * data for that chain. Powers cross-dimension tab filtering so clicking + * "Robinhood" hides venues that have no data on Robinhood. */ + venuesForChain?: Record; }; export type Benchmark = {