diff --git a/reproducibility/site/scripts/build-data.ts b/reproducibility/site/scripts/build-data.ts index 875da4b..d542cb0 100644 --- a/reproducibility/site/scripts/build-data.ts +++ b/reproducibility/site/scripts/build-data.ts @@ -94,6 +94,7 @@ interface RunDetail { params_hash: string; dataset_id: string; method_id: string; + method_display: string; model: string; retriever_id: string; retriever_display: string; @@ -195,11 +196,16 @@ function readRunDetails(retrievers: Record(); for (const r of dsRows) { const lm = logicalMethod(r.method_id, r.method_params_json); @@ -251,7 +256,7 @@ function buildPerDatasetViews( model_display: displayModel(r.model), retriever_id: r.retriever_id, retriever_display: r.retriever, - run_id: r.run_id, // populated/overwritten by the best cell + run_ids: {} as Record, // metric → run_id of the winning value metrics: {} as Record, best_for: {} as Record, }); @@ -259,13 +264,34 @@ function buildPerDatasetViews( const row = map.get(key); if (row.metrics[r.metric] === undefined || r.value > row.metrics[r.metric]) { row.metrics[r.metric] = r.value; - row.run_id = r.run_id; + row.run_ids[r.metric] = r.run_id; } } + // Discover which metrics actually exist in the data — the registry's + // eval_metrics is aspirational and may over-specify (e.g. MAP on DL, + // recall_1000 on BEIR). Render only what we have. + const present = new Set(); + for (const row of map.values()) { + for (const m of Object.keys(row.metrics)) present.add(m); + } + const allMetrics = Array.from(present); + const primary = present.has("ndcg_cut_10") ? "ndcg_cut_10" : allMetrics[0] ?? null; + const secondary = present.has("recall_1000") + ? "recall_1000" + : present.has("recall_100") + ? "recall_100" + : allMetrics.find((m) => m !== primary) ?? null; + // Order: primary first, secondary second, then anything else. + const orderedMetrics = [ + ...(primary ? [primary] : []), + ...(secondary && secondary !== primary ? [secondary] : []), + ...allMetrics.filter((m) => m !== primary && m !== secondary), + ]; + // best_for flags relative to the rows above. const list = Array.from(map.values()); - for (const m of allowed) { + for (const m of orderedMetrics) { let best = -Infinity; let bestRow: any = null; for (const row of list) { @@ -277,8 +303,10 @@ function buildPerDatasetViews( writeJSON(path.join(VIEWS_DIR, `dataset-${datasetId}.json`), { dataset_id: datasetId, - dataset: datasets[datasetId] ?? { id: datasetId, name: datasetId, eval_metrics: allowed }, - metric_columns: allowed, + dataset: datasets[datasetId] ?? { id: datasetId, name: datasetId, eval_metrics: orderedMetrics }, + metric_columns: orderedMetrics, + primary_metric: primary, + secondary_metric: secondary, runs: list, }); } diff --git a/reproducibility/site/src/components/FilterChips.astro b/reproducibility/site/src/components/FilterChips.astro new file mode 100644 index 0000000..a9aa251 --- /dev/null +++ b/reproducibility/site/src/components/FilterChips.astro @@ -0,0 +1,133 @@ +--- +/** + * Chip-style filter bar for any leaderboard table. + * + * Each group corresponds to a column on the table's attributes + * (e.g. data-method, data-model). Clicking a chip hides rows whose attribute + * doesn't match, by toggling the .qg-chip-hidden class and dispatching + * "qg-itable-reapply" on the nearest .qg-itable wrapper so InteractiveTable + * re-syncs its row-visibility + shown-count. + * + * The optional `metric` group is special: it swaps .qg-cell-primary / + * .qg-cell-secondary visibility and the matching column-label spans across + * the whole page, then re-keys cells' data-sort-value to the now-visible + * metric so sort follows what's on screen. + */ +interface ChipValue { + value: string; + label: string; +} +interface ChipGroup { + /** "method" | "model" | "retriever" | "metric"; matches */ + key: string; + /** Visible header text. */ + label: string; + /** First item is shown as the active default. For `metric`, use + * [{value:"primary", label:"nDCG@10"}, {value:"secondary", label:"Recall"}]. */ + values: ChipValue[]; +} +interface Props { + /** id of the table to filter (used to scope row queries to this table). */ + tableId: string; + groups: ChipGroup[]; +} +const { tableId, groups } = Astro.props; +--- + +
+ {groups.map((g) => ( +
+ {g.label}: +
+ {g.values.map((v, i) => ( + + ))} +
+
+ ))} +
+ + + + diff --git a/reproducibility/site/src/components/InteractiveTable.astro b/reproducibility/site/src/components/InteractiveTable.astro index f124377..03dc988 100644 --- a/reproducibility/site/src/components/InteractiveTable.astro +++ b/reproducibility/site/src/components/InteractiveTable.astro @@ -27,13 +27,28 @@ const initialSortAttr = initialSort
- - +
+ + +
+ 0 / 0 rows
@@ -48,16 +63,6 @@ const initialSortAttr = initialSort .qg-itable table thead th[data-sort-skip] { cursor: default; } - .qg-itable table thead th .qg-sort-arrow { - opacity: 0.35; - margin-left: 0.25rem; - font-size: 0.7rem; - } - .qg-itable table thead th[data-sort-dir="asc"] .qg-sort-arrow, - .qg-itable table thead th[data-sort-dir="desc"] .qg-sort-arrow { - opacity: 1; - color: var(--qg-accent); - } diff --git a/reproducibility/site/src/pages/methods/[id].astro b/reproducibility/site/src/pages/methods/[id].astro index 9c5c4d8..6e14178 100644 --- a/reproducibility/site/src/pages/methods/[id].astro +++ b/reproducibility/site/src/pages/methods/[id].astro @@ -2,6 +2,8 @@ import Default from "../../layouts/Default.astro"; import EmptyState from "../../components/EmptyState.astro"; import InteractiveTable from "../../components/InteractiveTable.astro"; +import FilterChips from "../../components/FilterChips.astro"; +import MatrixCell from "../../components/MatrixCell.astro"; import methods from "../../data/methods.json"; const shards = import.meta.glob<{ default: any }>( @@ -21,7 +23,7 @@ export async function getStaticPaths() { const { id } = Astro.params; const view = shardFor(id!); const meta = methods.find((m: any) => m.id === id); -const rows = view?.rows ?? []; +const rows = (view?.rows ?? []) as any[]; const SHORT: Record = { "msmarco-v1-passage.trecdl2019": "DL 2019", @@ -35,12 +37,20 @@ const SHORT: Record = { "beir-v1.0.0-trec-news": "News", }; const METRIC_LABEL: Record = { - ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", + ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", map: "MAP", }; const datasetCols = (await import("../../data/matrix.json")).default.dataset_columns; - const title = view?.method_display ?? meta?.display ?? id ?? "Method"; +const tableId = "qg-method-table"; + +const uniq = (xs: any[], key: string, displayKey?: string) => { + const m = new Map(); + for (const r of xs) m.set(r[key], r[displayKey ?? key] ?? r[key]); + return Array.from(m.entries()).sort((a, b) => a[0].localeCompare(b[0])); +}; +const modelChoices = uniq(rows, "model", "model_display"); +const retrieverChoices = uniq(rows, "retriever_id", "retriever_display"); --- @@ -49,50 +59,68 @@ const title = view?.method_display ?? meta?.display ?? id ?? "Method";
{id}
{rows.length} model × retriever combinations
- { - rows.length === 0 ? ( -
- + {rows.length === 0 ? ( +
+ +
+ ) : ( + <> +
+ ({ value: v, label: l }))] }, + { key: "retriever", label: "Retriever", + values: [{ value: "", label: "All" }, ...retrieverChoices.map(([v, l]) => ({ value: v, label: l }))] }, + { key: "metric", label: "Metric", + values: [{ value: "primary", label: "nDCG@10" }, { value: "secondary", label: "Recall" }] }, + ]} + />
- ) : ( -
- -
- - + + +
+
+
+ - - + + {datasetCols.map((d: any) => ( - ))} {rows.map((row: any) => ( - - - + + + {datasetCols.map((d: any) => { const cell = row.values?.[d.id] ?? {}; - const runId = row.run_ids?.[d.id]; - const primary = cell[d.primary_metric]; - const v = primary?.value ?? ""; return ( - + ); })} @@ -100,8 +128,8 @@ const title = view?.method_display ?? meta?.display ?? id ?? "Method";
ModelRetrieverModelRetriever - {SHORT[d.id] ?? d.name} - / {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} + +
{SHORT[d.id] ?? d.name}
+
+ {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} +
+
{row.model_display ?? row.model}{row.retriever_display ?? row.retriever_id}
{row.model_display ?? row.model}{row.retriever_display ?? row.retriever_id} - {runId && primary !== undefined ? ( - - - {primary.value.toFixed(3)} - - - ) : ( - - )} -
-
-
- ) - } +
+ + + )}
diff --git a/reproducibility/site/src/pages/models/[id].astro b/reproducibility/site/src/pages/models/[id].astro index 9973e86..b7ae9c9 100644 --- a/reproducibility/site/src/pages/models/[id].astro +++ b/reproducibility/site/src/pages/models/[id].astro @@ -2,6 +2,8 @@ import Default from "../../layouts/Default.astro"; import EmptyState from "../../components/EmptyState.astro"; import InteractiveTable from "../../components/InteractiveTable.astro"; +import FilterChips from "../../components/FilterChips.astro"; +import MatrixCell from "../../components/MatrixCell.astro"; import models from "../../data/models.json"; const shards = import.meta.glob<{ default: any }>( @@ -21,9 +23,8 @@ export async function getStaticPaths() { const { id } = Astro.params; const view = shardFor(id!); const meta = models.find((m: any) => m.slug === id); -const rows = view?.rows ?? []; +const rows = (view?.rows ?? []) as any[]; -// Same dataset-label/metric-label tables as the home page. const SHORT: Record = { "msmarco-v1-passage.trecdl2019": "DL 2019", "msmarco-v1-passage.trecdl2020": "DL 2020", @@ -36,13 +37,20 @@ const SHORT: Record = { "beir-v1.0.0-trec-news": "News", }; const METRIC_LABEL: Record = { - ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", + ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", map: "MAP", }; -// Dataset columns from the shared home matrix shape (re-derive from rows). const datasetCols = (await import("../../data/matrix.json")).default.dataset_columns; - const title = meta?.display ?? view?.model ?? id ?? "Model"; +const tableId = "qg-model-table"; + +const uniq = (xs: any[], key: string, displayKey?: string) => { + const m = new Map(); + for (const r of xs) m.set(r[key], r[displayKey ?? key] ?? r[key]); + return Array.from(m.entries()).sort((a, b) => a[0].localeCompare(b[0])); +}; +const methodChoices = uniq(rows, "method_id", "method_display"); +const retrieverChoices = uniq(rows, "retriever_id", "retriever_display"); --- @@ -50,50 +58,68 @@ const title = meta?.display ?? view?.model ?? id ?? "Model";

{title}

{rows.length} method × retriever combinations
- { - rows.length === 0 ? ( -
- + {rows.length === 0 ? ( +
+ +
+ ) : ( + <> +
+ ({ value: v, label: l }))] }, + { key: "retriever", label: "Retriever", + values: [{ value: "", label: "All" }, ...retrieverChoices.map(([v, l]) => ({ value: v, label: l }))] }, + { key: "metric", label: "Metric", + values: [{ value: "primary", label: "nDCG@10" }, { value: "secondary", label: "Recall" }] }, + ]} + />
- ) : ( -
- -
- - + + +
+
+
+ - - + + {datasetCols.map((d: any) => ( - ))} {rows.map((row: any) => ( - - - + + + {datasetCols.map((d: any) => { const cell = row.values?.[d.id] ?? {}; - const runId = row.run_ids?.[d.id]; - const primary = cell[d.primary_metric]; - const v = primary?.value ?? ""; return ( - + ); })} @@ -101,8 +127,8 @@ const title = meta?.display ?? view?.model ?? id ?? "Model";
MethodRetrieverMethodRetriever - {SHORT[d.id] ?? d.name} - / {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} + +
{SHORT[d.id] ?? d.name}
+
+ {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} +
+
{row.method_display ?? row.method_id}{row.retriever_display ?? row.retriever_id}
{row.method_display ?? row.method_id}{row.retriever_display ?? row.retriever_id} - {runId && primary !== undefined ? ( - - - {primary.value.toFixed(3)} - - - ) : ( - - )} -
-
-
- ) - } +
+ + + )}
diff --git a/reproducibility/site/src/pages/models/index.astro b/reproducibility/site/src/pages/models/index.astro index b0a7f60..39b5203 100644 --- a/reproducibility/site/src/pages/models/index.astro +++ b/reproducibility/site/src/pages/models/index.astro @@ -18,7 +18,7 @@ import models from "../../data/models.json"; {models.map((m: any) => (
  • -
    {m.id}
    +
    {m.display ?? m.id}
    {m.run_count} run{m.run_count === 1 ? "" : "s"}
    diff --git a/reproducibility/site/src/pages/retrievers/[id].astro b/reproducibility/site/src/pages/retrievers/[id].astro index d089dd9..5166c7b 100644 --- a/reproducibility/site/src/pages/retrievers/[id].astro +++ b/reproducibility/site/src/pages/retrievers/[id].astro @@ -2,6 +2,8 @@ import Default from "../../layouts/Default.astro"; import EmptyState from "../../components/EmptyState.astro"; import InteractiveTable from "../../components/InteractiveTable.astro"; +import FilterChips from "../../components/FilterChips.astro"; +import MatrixCell from "../../components/MatrixCell.astro"; import retrievers from "../../data/retrievers.json"; const shards = import.meta.glob<{ default: any }>( @@ -21,7 +23,7 @@ export async function getStaticPaths() { const { id } = Astro.params; const view = shardFor(id!); const meta = retrievers.find((r: any) => r.id === id); -const rows = view?.rows ?? []; +const rows = (view?.rows ?? []) as any[]; const SHORT: Record = { "msmarco-v1-passage.trecdl2019": "DL 2019", @@ -35,11 +37,20 @@ const SHORT: Record = { "beir-v1.0.0-trec-news": "News", }; const METRIC_LABEL: Record = { - ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", + ndcg_cut_10: "nDCG@10", recall_1000: "R@1k", recall_100: "R@100", map: "MAP", }; const datasetCols = (await import("../../data/matrix.json")).default.dataset_columns; const title = meta?.display_name ?? id ?? "Retriever"; +const tableId = "qg-retriever-table"; + +const uniq = (xs: any[], key: string, displayKey?: string) => { + const m = new Map(); + for (const r of xs) m.set(r[key], r[displayKey ?? key] ?? r[key]); + return Array.from(m.entries()).sort((a, b) => a[0].localeCompare(b[0])); +}; +const methodChoices = uniq(rows, "method_id", "method_display"); +const modelChoices = uniq(rows, "model", "model_display"); --- @@ -48,50 +59,68 @@ const title = meta?.display_name ?? id ?? "Retriever";
    {id} · {meta?.paradigm}
    {rows.length} method × model combinations
    - { - rows.length === 0 ? ( -
    - + {rows.length === 0 ? ( +
    + +
    + ) : ( + <> +
    + ({ value: v, label: l }))] }, + { key: "model", label: "Model", + values: [{ value: "", label: "All" }, ...modelChoices.map(([v, l]) => ({ value: v, label: l }))] }, + { key: "metric", label: "Metric", + values: [{ value: "primary", label: "nDCG@10" }, { value: "secondary", label: "Recall" }] }, + ]} + />
    - ) : ( -
    - -
    - - + + +
    +
    +
    + - - + + {datasetCols.map((d: any) => ( - ))} {rows.map((row: any) => ( - - - + + + {datasetCols.map((d: any) => { const cell = row.values?.[d.id] ?? {}; - const runId = row.run_ids?.[d.id]; - const primary = cell[d.primary_metric]; - const v = primary?.value ?? ""; return ( - + ); })} @@ -99,8 +128,8 @@ const title = meta?.display_name ?? id ?? "Retriever";
    MethodModelMethodModel - {SHORT[d.id] ?? d.name} - / {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} + +
    {SHORT[d.id] ?? d.name}
    +
    + {METRIC_LABEL[d.primary_metric] ?? d.primary_metric} +
    +
    {row.method_display ?? row.method_id}{row.model_display ?? row.model}
    {row.method_display ?? row.method_id}{row.model_display ?? row.model} - {runId && primary !== undefined ? ( - - - {primary.value.toFixed(3)} - - - ) : ( - - )} -
    -
    -
    - ) - } +
    + + + )}
    diff --git a/reproducibility/site/src/pages/runs/[run_id].astro b/reproducibility/site/src/pages/runs/[run_id].astro index 77dae55..423daaf 100644 --- a/reproducibility/site/src/pages/runs/[run_id].astro +++ b/reproducibility/site/src/pages/runs/[run_id].astro @@ -12,14 +12,16 @@ export async function getStaticPaths() { const { run_id } = Astro.params; const run = (runs as Record)[run_id!]; -// Compose the reproduce snippets from the run's config. const cfg = (run?.config ?? {}) as any; const retrieval = (cfg.retrieval ?? {}) as any; const dsCfg = (cfg.dataset_config ?? {}) as any; const methodParams = (cfg.method_params ?? {}) as Record; const llm = (cfg.llm_config ?? {}) as Record; -// Pretty-format method_params for the Python snippet (skipping noise/secrets). +// method_params we surface in the reproduce snippet — strip locally-pathy noise +// (collection_path / train_queries_path etc.) that won't apply on a fresh +// checkout. Mode + num_examples + train_split are the knobs that change +// behavior across runs. const FILTERED_PARAM_KEYS = new Set([ "judge_rel_mode", "collection_path", @@ -31,66 +33,68 @@ const cleanParams: Record = {}; for (const [k, v] of Object.entries(methodParams)) { if (!FILTERED_PARAM_KEYS.has(k)) cleanParams[k] = v; } -const paramKwargs = Object.entries(cleanParams) - .map(([k, v]) => ` ${k}=${JSON.stringify(v)},`) - .join("\n"); - -const pyReformulate = `import querygym as qg - -# 1. Build the reformulator with this run's method + model + params. -reformulator = qg.create_reformulator( - "${run.method_id}", - model="${run.model}", - temperature=${llm.temperature ?? 1.0}, - max_tokens=${llm.max_tokens ?? 128},${paramKwargs ? "\n" + paramKwargs : ""} -) - -# 2. Reformulate queries from the dataset's topics file. -# (Pyserini topic name: ${dsCfg.topics ?? ""} ; ${dsCfg.num_queries ?? "?"} queries) -queries = qg.loaders.load_pyserini_topics("${dsCfg.topics ?? ""}") -reformulated = reformulator.reformulate_batch(queries) - -# 3. Write the reformulated queries TSV (this becomes --topics for retrieval). -qg.loaders.save_topics_tsv(reformulated, "reformulated_queries.tsv") -`; - -// Retrieval command depends on retriever paradigm. -const retrId = retrieval.retriever_id ?? ""; +const methodParamsJson = Object.keys(cleanParams).length + ? JSON.stringify(cleanParams) + : null; + +// Reproduce step 1: run the example pipeline to reformulate queries. +// This is the same script that produced the leaderboard row, so the resulting +// reformulated_queries.tsv is byte-identical given the same model + seed. +const pipelineCmd = `python examples/querygym_pyserini/pipeline.py \\ + --dataset ${run.dataset_id} \\ + --method ${run.method_id} \\ + --model ${run.model} \\ + --steps reformulate \\ + --temperature ${llm.temperature ?? 1.0} \\ + --max-tokens ${llm.max_tokens ?? 128} \\${methodParamsJson ? ` + --method-params '${methodParamsJson}' \\` : ""} + --output-dir outputs/reproduce`; + +// Reproduce step 2: retrieval. Index names differ per paradigm — Pyserini +// publishes a base BM25 index plus suffixed SPLADE/BGE variants. const paradigm = retrieval.paradigm ?? ""; const params = (retrieval.params ?? {}) as any; -const queriesPath = run.artifacts.queries_url - ? `# ↑ download from the leaderboard's queries link, or regenerate with the Python above\nreformulated_queries.tsv` - : "reformulated_queries.tsv"; +// Strip the trailing ".flat" segment BEIR BM25 indexes carry; SPLADE/BGE +// indexes for the same dataset live under . not .flat.. +const baseIndex = String(dsCfg.index ?? "").replace(/\.flat$/, ""); let retrievalCmd = ""; if (paradigm === "lexical") { retrievalCmd = `python -m pyserini.search.lucene \\ --threads 16 --batch-size 128 \\ --index ${dsCfg.index ?? ""} \\ - --topics reformulated_queries.tsv \\ + --topics outputs/reproduce/queries/reformulated_queries.tsv \\ --bm25 --k1 ${params.k1 ?? 0.9} --b ${params.b ?? 0.4} \\ --output run.txt \\ --hits 1000`; } else if (paradigm === "learned_sparse") { retrievalCmd = `python -m pyserini.search.lucene \\ --threads 16 --batch-size 128 \\ - --index ${dsCfg.index ?? ""}.splade-pp-ed \\ - --topics reformulated_queries.tsv \\ + --index ${baseIndex || ""}.splade-pp-ed \\ + --topics outputs/reproduce/queries/reformulated_queries.tsv \\ --encoder ${params.model ?? "naver/splade-cocondenser-ensembledistil"} \\ --output run.txt \\ --hits 1000 --impact`; } else if (paradigm === "dense") { retrievalCmd = `python -m pyserini.search.faiss \\ --threads 16 --batch-size 128 \\ - --index ${dsCfg.index ?? ""}.bge-base-en-v1.5 \\ - --topics reformulated_queries.tsv \\ + --index ${baseIndex || ""}.bge-base-en-v1.5 \\ + --topics outputs/reproduce/queries/reformulated_queries.tsv \\ --encoder ${params.encoder ?? "BAAI/bge-base-en-v1.5"} \\ --output run.txt \\ --hits 1000`; } -const trecEvalCmd = `python -m pyserini.eval.trec_eval -c -m ${run.metrics ? Object.keys(run.metrics).join(" -m ").replace(/_/g, ".") : "ndcg_cut.10"} \\ - ${dsCfg.topics ?? ""} run.txt`; +// Reproduce step 3: evaluate. trec_eval reads the prebuilt qrels key from the +// dataset registry (qrels.name), which Pyserini resolves to the canonical +// qrels file. Metric flags map from this run's stored metric ids. +const trecMetrics = Object.keys(run.metrics ?? {}) + .map((m) => m.replace(/_/g, ".")) + .join(" -m "); +// For BEIR + DL, the prebuilt qrels key matches the topics key; reuse it. +const qrelsName = dsCfg.topics ?? ""; +const trecEvalCmd = `python -m pyserini.eval.trec_eval -c -m ${trecMetrics || "ndcg_cut.10"} \\ + ${qrelsName} run.txt`; --- @@ -100,7 +104,7 @@ const trecEvalCmd = `python -m pyserini.eval.trec_eval -c -m ${run.metrics ? Obj
    - + @@ -124,14 +128,15 @@ const trecEvalCmd = `python -m pyserini.eval.trec_eval -c -m ${run.metrics ? Obj

    Reproduce this run

    - Two steps: (1) reformulate the queries with QueryGym, (2) run retrieval with Pyserini. + Three steps: (1) reformulate the queries with QueryGym's example pipeline, + (2) run retrieval with Pyserini, (3) evaluate with trec_eval.

    - {pyReformulate} + {pipelineCmd} {retrievalCmd && ( - {retrievalCmd} + {retrievalCmd} )} - {trecEvalCmd} + {trecEvalCmd}
    @@ -162,23 +167,6 @@ const trecEvalCmd = `python -m pyserini.eval.trec_eval -c -m ${run.metrics ? Obj

    Config

    -
    {JSON.stringify(run.config, null, 2)}
    + {JSON.stringify(run.config, null, 2)}
    - - diff --git a/reproducibility/site/src/styles/global.css b/reproducibility/site/src/styles/global.css index 2b10966..90940d2 100644 --- a/reproducibility/site/src/styles/global.css +++ b/reproducibility/site/src/styles/global.css @@ -10,4 +10,120 @@ .qg-card { @apply rounded-xl border border-qg-border bg-qg-bg-soft p-6 transition hover:border-qg-accent; } + + /* Leaderboard table — the card that wraps a scrollable table with sticky + * thead and sticky axis columns. Fixed height keeps the filter card + + * page chrome in view while rows scroll inside. */ + .qg-table-card { + @apply flex h-[600px] flex-col overflow-hidden rounded-xl border border-qg-border bg-qg-bg-soft; + } + .qg-table-scroll { + @apply flex-grow overflow-auto; + } + + /* Filter strip — wraps chips for axes + metric toggle in one card. */ + .qg-filter-card { + @apply mb-4 rounded-xl border border-qg-border bg-qg-bg-soft p-4; + } +} + +/* ---------- Scrollbar styling ------------------------------------------- */ + +.qg-table-scroll::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.qg-table-scroll::-webkit-scrollbar-track { + background: var(--qg-bg-soft); +} +.qg-table-scroll::-webkit-scrollbar-thumb { + background: var(--qg-border); + border-radius: 4px; +} +.qg-table-scroll::-webkit-scrollbar-thumb:hover { + background: var(--qg-fg-muted); +} +.qg-table-scroll { + scrollbar-width: thin; + scrollbar-color: var(--qg-border) var(--qg-bg-soft); +} + +/* ---------- Sticky thead inside the scroll container ------------------- */ + +.qg-table-scroll thead th { + position: sticky; + top: 0; + z-index: 10; + background: var(--qg-bg-soft); + box-shadow: inset 0 -1px 0 var(--qg-border); +} + +/* ---------- Sticky axis columns ---------------------------------------- * + * Each page declares per-table widths via inline style on the : + * style="--qg-axis-w-1: 120px; --qg-axis-w-2: 180px;" + * Then applies .qg-axis-1 / -2 / -3 on the relevant
    + . */ + +.qg-axis-1, +.qg-axis-2, +.qg-axis-3 { + position: sticky; + z-index: 20; + background: var(--qg-bg-soft); +} +.qg-axis-1 { left: 0; min-width: var(--qg-axis-w-1, 120px); } +.qg-axis-2 { left: var(--qg-axis-w-1, 120px); min-width: var(--qg-axis-w-2, 180px); } +.qg-axis-3 { + left: calc(var(--qg-axis-w-1, 120px) + var(--qg-axis-w-2, 180px)); + border-right: 1px solid var(--qg-border); +} + +/* Body cells: lower z so sticky thead wins; different bg so columns pop + * against the soft-bg header. Hover keeps the columns in sync with the row. */ +.qg-table-scroll tbody td.qg-axis-1, +.qg-table-scroll tbody td.qg-axis-2, +.qg-table-scroll tbody td.qg-axis-3 { + background: var(--qg-bg); + z-index: 5; +} +.qg-table-scroll tbody tr:hover td.qg-axis-1, +.qg-table-scroll tbody tr:hover td.qg-axis-2, +.qg-table-scroll tbody tr:hover td.qg-axis-3 { + background: var(--qg-row-hover); +} + +/* On narrow viewports the sticky columns would eat the whole screen; drop + * back to standard scroll so users can reach the data columns. */ +@media (max-width: 768px) { + .qg-axis-1, + .qg-axis-2, + .qg-axis-3 { + position: static; + min-width: 0; + } + .qg-axis-3 { border-right: none; } +} + +/* ---------- Best-cell highlight ---------------------------------------- */ + +.qg-cell-best { + color: var(--qg-accent); + font-weight: 700; +} +[data-theme="dark"] .qg-cell-best { + text-shadow: 0 0 6px rgba(236, 72, 153, 0.4); +} + +/* ---------- Sort-arrow polish on InteractiveTable thead ---------------- */ + +.qg-itable table thead th .qg-sort-arrow { + opacity: 0.4; + font-size: 0.75rem; + margin-left: 0.25rem; +} +.qg-itable table thead th:hover .qg-sort-arrow { + opacity: 0.7; +} +.qg-itable table thead th[data-sort-dir] .qg-sort-arrow { + opacity: 1; + color: var(--qg-accent); }