From ff62e38affdfe1529c894953ad4dd72ecf11acb1 Mon Sep 17 00:00:00 2001 From: Jared Smith Date: Wed, 1 Jul 2026 11:42:10 -0700 Subject: [PATCH 1/8] building out funtionality for dataset and global leaderboards --- docusaurus.config.ts | 5 + scripts/generate-datasets.mjs | 74 ++++++ .../DatasetMetadataModal.module.css | 48 +++- src/components/DatasetMetadataModal.tsx | 48 ++++ src/lib/performance.ts | 228 ++++++++++++++++ src/pages/datasets/index.module.css | 10 +- src/pages/leaderboard/index.module.css | 251 ++++++++++++++++++ src/pages/leaderboard/index.tsx | 208 +++++++++++++++ 8 files changed, 866 insertions(+), 6 deletions(-) create mode 100644 src/lib/performance.ts create mode 100644 src/pages/leaderboard/index.module.css create mode 100644 src/pages/leaderboard/index.tsx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index a6bafd9..81e2154 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -73,6 +73,7 @@ const config: Config = { label: 'Docs', }, {to: '/datasets', label: 'Dataset Search', position: 'left'}, + {to: '/leaderboard', label: 'Leaderboard', position: 'left'}, { href: 'https://github.com/Project-AgML/AgML', label: 'GitHub', @@ -99,6 +100,10 @@ const config: Config = { label: 'Dataset Search', to: '/datasets', }, + { + label: 'Leaderboard', + to: '/leaderboard', + }, { label: 'AgML Library', href: 'https://github.com/Project-AgML/AgML', diff --git a/scripts/generate-datasets.mjs b/scripts/generate-datasets.mjs index 083022f..65cda1a 100644 --- a/scripts/generate-datasets.mjs +++ b/scripts/generate-datasets.mjs @@ -7,6 +7,10 @@ const projectRoot = path.resolve(__dirname, '..'); const staticDataDir = path.join(projectRoot, 'static', 'data'); const datasetsPath = path.join(staticDataDir, 'datasets.json'); const hfDatasetsPath = path.join(staticDataDir, 'hf_datasets.json'); +const performanceDir = path.join(staticDataDir, 'performance'); +const performanceIndexPath = path.join(performanceDir, 'index.json'); +const performanceGlobalPath = path.join(performanceDir, 'global.json'); +const PERFORMANCE_MANIFEST_FILES = new Set(['index.json', 'global.json']); function readJson(filePath) { if (!fs.existsSync(filePath)) return null; @@ -31,6 +35,61 @@ function writeJson(filePath, value) { fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8'); } +function buildDatasetMetadataLookup(...manifests) { + const lookup = new Map(); + for (const manifest of manifests) { + for (const entry of manifest ?? []) { + const name = typeof entry?.name === 'string' ? entry.name : null; + if (!name || lookup.has(name)) continue; + const cropTypes = Array.isArray(entry.crop_types) + ? entry.crop_types.filter((c) => typeof c === 'string' && c.trim()).map((c) => c.toLowerCase()) + : null; + const mlTask = typeof entry.machine_learning_task === 'string' && entry.machine_learning_task.trim() + ? entry.machine_learning_task + : null; + lookup.set(name, { crop_types: cropTypes?.length ? cropTypes : null, machine_learning_task: mlTask }); + } + } + return lookup; +} + +function sortLeaderboardEntries(entries) { + return [...entries].sort((a, b) => { + const rankA = typeof a.rank === 'number' ? a.rank : null; + const rankB = typeof b.rank === 'number' ? b.rank : null; + if (rankA != null && rankB != null) return rankA - rankB; + const scoreA = typeof a.score === 'number' ? a.score : null; + const scoreB = typeof b.score === 'number' ? b.score : null; + if (scoreA != null && scoreB != null) return scoreB - scoreA; + return 0; + }); +} + +function buildGlobalPerformanceRecords(performanceDatasets, metadataLookup) { + const records = []; + for (const datasetName of performanceDatasets) { + const raw = readJson(path.join(performanceDir, `${datasetName}.json`)); + const leaderboard = Array.isArray(raw) ? raw : Array.isArray(raw?.leaderboard) ? raw.leaderboard : []; + const entries = sortLeaderboardEntries(leaderboard.filter((entry) => typeof entry?.model === 'string' && entry.model.trim())); + const total = entries.length; + if (total === 0) continue; + + const meta = metadataLookup.get(datasetName) ?? { crop_types: null, machine_learning_task: null }; + entries.forEach((entry, index) => { + const rank = index + 1; + const percentile = total > 1 ? ((total - rank) / (total - 1)) * 100 : 100; + records.push({ + model: entry.model.trim(), + dataset: datasetName, + percentile, + crop_types: meta.crop_types, + machine_learning_task: meta.machine_learning_task, + }); + }); + } + return records; +} + async function generateDatasets() { const datasets = normalizeManifest(readJson(datasetsPath)); const hfDatasetsRaw = readJson(hfDatasetsPath); @@ -45,6 +104,21 @@ async function generateDatasets() { writeJson(hfDatasetsPath, hfDatasets); console.log('Wrote', hfDatasetsPath); } + + const performanceDatasets = fs.existsSync(performanceDir) + ? fs + .readdirSync(performanceDir) + .filter((file) => file.endsWith('.json') && !PERFORMANCE_MANIFEST_FILES.has(file)) + .map((file) => file.slice(0, -'.json'.length)) + .sort() + : []; + writeJson(performanceIndexPath, performanceDatasets); + console.log('Wrote', performanceIndexPath, '—', performanceDatasets.length, 'performance datasets'); + + const metadataLookup = buildDatasetMetadataLookup(datasets, hfDatasets); + const globalPerformance = buildGlobalPerformanceRecords(performanceDatasets, metadataLookup); + writeJson(performanceGlobalPath, globalPerformance); + console.log('Wrote', performanceGlobalPath, '—', globalPerformance.length, 'performance records'); } generateDatasets().catch((err) => { diff --git a/src/components/DatasetMetadataModal.module.css b/src/components/DatasetMetadataModal.module.css index dc1ff3e..42a81a6 100644 --- a/src/components/DatasetMetadataModal.module.css +++ b/src/components/DatasetMetadataModal.module.css @@ -10,7 +10,7 @@ } .modal { - width: min(780px, 100%); + width: min(1200px, 92vw); max-height: min(88vh, 920px); overflow: auto; border-radius: 28px; @@ -211,6 +211,52 @@ outline: none; } +.leaderboardWrap { + overflow-x: auto; +} + +.leaderboardTable { + width: 100%; + margin-top: 0.6rem; + border-collapse: collapse; + font-size: 0.88rem; +} + +.leaderboardTable th, +.leaderboardTable td { + padding: 0.55rem 0.7rem; + text-align: left; + border-bottom: 1px solid var(--agml-border); + white-space: nowrap; +} + +.leaderboardTable th { + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--agml-muted); +} + +.leaderboardTable td { + color: var(--agml-text); +} + +.leaderboardTable tbody tr:last-child td { + border-bottom: none; +} + +.leaderboardTable a { + color: var(--ifm-color-primary); + font-weight: 600; + text-decoration: none; +} + +.leaderboardTable a:hover, +.leaderboardTable a:focus-visible { + text-decoration: underline; +} + :global(html[data-theme='dark']) .backdrop { background: rgba(4, 7, 5, 0.72); } diff --git a/src/components/DatasetMetadataModal.tsx b/src/components/DatasetMetadataModal.tsx index d5e38ee..1e67699 100644 --- a/src/components/DatasetMetadataModal.tsx +++ b/src/components/DatasetMetadataModal.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import type { Dataset } from '../lib/datasets'; import { formatDisplayLocation } from '../lib/datasets'; +import { useDatasetPerformance } from '../lib/performance'; import styles from './DatasetMetadataModal.module.css'; function formatImageCount(count: number | null) { @@ -88,6 +89,8 @@ export function DatasetMetadataModal({ }; }, [open, onClose]); + const datasetPerformance = useDatasetPerformance(open ? (dataset?.name ?? null) : null); + if (!open || dataset == null) return null; const detailRows = [ @@ -192,6 +195,51 @@ export function DatasetMetadataModal({ )} )} + +
+

Model performance leaderboard

+ {datasetPerformance.loading ? ( +

Loading leaderboard…

+ ) : datasetPerformance.data && datasetPerformance.data.entries.length > 0 ? ( +
+ {datasetPerformance.data.metric && ( +

+ Metric: {datasetPerformance.data.metric} +

+ )} + + + + + + + + + + + {datasetPerformance.data.entries.map((entry, index) => ( + + + + + + + ))} + +
RankModelScoreSubmitted by
{entry.rank ?? index + 1} + {entry.link ? ( + + {entry.model} + + ) : ( + entry.model + )} + {entry.score != null ? entry.score.toLocaleString() : '—'}{entry.submitted_by ?? '—'}
+
+ ) : ( +

No leaderboard results have been submitted for this dataset yet.

+ )} +
diff --git a/src/lib/performance.ts b/src/lib/performance.ts new file mode 100644 index 0000000..b710d10 --- /dev/null +++ b/src/lib/performance.ts @@ -0,0 +1,228 @@ +import { useEffect, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export interface PerformanceEntry { + rank: number | null; + model: string; + score: number | null; + submitted_by: string | null; + date: string | null; + link: string | null; + notes: string | null; +} + +export interface DatasetPerformance { + metric: string | null; + entries: PerformanceEntry[]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function toNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string' && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function toText(value: unknown): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +function normalizeEntry(raw: unknown): PerformanceEntry | null { + if (!isRecord(raw)) return null; + const model = toText(raw.model ?? raw.model_name ?? raw.name); + if (!model) return null; + + return { + rank: toNumber(raw.rank), + model, + score: toNumber(raw.score ?? raw.value ?? raw.metric_value), + submitted_by: toText(raw.submitted_by ?? raw.submittedBy ?? raw.author), + date: toText(raw.date ?? raw.submitted_at), + link: toText(raw.link ?? raw.url ?? raw.source_link), + notes: toText(raw.notes), + }; +} + +function normalizePerformance(json: unknown): DatasetPerformance { + const rawEntries = Array.isArray(json) + ? json + : isRecord(json) && Array.isArray(json.leaderboard) + ? json.leaderboard + : []; + const entries = rawEntries + .map(normalizeEntry) + .filter((entry): entry is PerformanceEntry => entry != null) + .sort((a, b) => { + if (a.rank != null && b.rank != null) return a.rank - b.rank; + if (a.score != null && b.score != null) return b.score - a.score; + return 0; + }); + + const metric = isRecord(json) ? toText(json.metric) : null; + + return { metric, entries }; +} + +export interface GlobalPerformanceRecord { + model: string; + dataset: string; + percentile: number; + crop_types: string[] | null; + machine_learning_task: string | null; +} + +export interface GlobalLeaderboardEntry { + model: string; + averagePercentile: number; + appearances: number; + datasets: string[]; +} + +function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord | null { + if (!isRecord(raw)) return null; + const model = toText(raw.model); + const dataset = toText(raw.dataset); + const percentile = toNumber(raw.percentile); + if (!model || !dataset || percentile == null) return null; + + const cropTypes = Array.isArray(raw.crop_types) + ? raw.crop_types.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) + : null; + + return { + model, + dataset, + percentile, + crop_types: cropTypes?.length ? cropTypes : null, + machine_learning_task: toText(raw.machine_learning_task), + }; +} + +export function computeGlobalLeaderboard( + records: GlobalPerformanceRecord[], + options: { cropTypes?: string[]; mlTasks?: string[]; minAppearances?: number } = {} +): GlobalLeaderboardEntry[] { + const { cropTypes = [], mlTasks = [], minAppearances = 3 } = options; + const stats = new Map }>(); + + for (const record of records) { + if (cropTypes.length && !record.crop_types?.some((crop) => cropTypes.includes(crop))) continue; + if (mlTasks.length && !(record.machine_learning_task && mlTasks.includes(record.machine_learning_task))) continue; + + const entryStats = stats.get(record.model) ?? { totalPercentile: 0, appearances: 0, datasets: new Set() }; + entryStats.totalPercentile += record.percentile; + entryStats.appearances += 1; + entryStats.datasets.add(record.dataset); + stats.set(record.model, entryStats); + } + + return Array.from(stats.entries()) + .filter(([, entryStats]) => entryStats.appearances >= minAppearances) + .map(([model, entryStats]) => ({ + model, + averagePercentile: entryStats.totalPercentile / entryStats.appearances, + appearances: entryStats.appearances, + datasets: Array.from(entryStats.datasets).sort(), + })) + .sort((a, b) => b.averagePercentile - a.averagePercentile); +} + +export function useGlobalPerformance(): { + data: GlobalPerformanceRecord[]; + loading: boolean; + error: Error | null; +} { + const url = useBaseUrl('/data/performance/global.json'); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + setLoading(true); + setError(null); + + fetch(url) + .then((response) => { + if (!response.ok) throw new Error('Failed to load global performance data'); + return response.json(); + }) + .then((json: unknown) => { + if (!active) return; + const records = Array.isArray(json) ? json.map(normalizeGlobalPerformanceRecord).filter((entry): entry is GlobalPerformanceRecord => entry != null) : []; + setData(records); + }) + .catch((err) => { + if (!active) return; + setError(err instanceof Error ? err : new Error('Failed to load global performance data')); + }) + .finally(() => { + if (!active) return; + setLoading(false); + }); + + return () => { + active = false; + }; + }, [url]); + + return { data, loading, error }; +} + +export function useDatasetPerformance(datasetName: string | null): { + data: DatasetPerformance | null; + loading: boolean; + error: Error | null; +} { + const url = useBaseUrl(`/data/performance/${datasetName ?? ''}.json`); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!datasetName) { + setData(null); + setLoading(false); + setError(null); + return; + } + + let active = true; + setLoading(true); + setError(null); + + fetch(url) + .then((response) => { + if (response.status === 404) return null; + if (!response.ok) throw new Error(`Failed to load performance data for ${datasetName}`); + return response.json(); + }) + .then((json) => { + if (!active) return; + setData(json == null ? { metric: null, entries: [] } : normalizePerformance(json)); + }) + .catch((err) => { + if (!active) return; + setError(err instanceof Error ? err : new Error('Failed to load performance data')); + setData(null); + }) + .finally(() => { + if (!active) return; + setLoading(false); + }); + + return () => { + active = false; + }; + }, [datasetName, url]); + + return { data, loading, error }; +} diff --git a/src/pages/datasets/index.module.css b/src/pages/datasets/index.module.css index aa0be49..dc0149c 100644 --- a/src/pages/datasets/index.module.css +++ b/src/pages/datasets/index.module.css @@ -45,7 +45,7 @@ .hero { position: relative; z-index: 1; - max-width: 1100px; + max-width: 1600px; margin: 0 auto 2.5rem; padding: 2.5rem 2.5rem 2rem; border-radius: 28px; @@ -112,7 +112,7 @@ .controls { position: relative; z-index: 30; - max-width: 1100px; + max-width: 1600px; margin: 0 auto 1.5rem; display: flex; flex-wrap: wrap; @@ -259,7 +259,7 @@ .chipsSection { position: relative; z-index: 1; - max-width: 1100px; + max-width: 1600px; margin: 0 auto 1.5rem; display: flex; flex-direction: column; @@ -336,7 +336,7 @@ .activeFilters { position: relative; z-index: 1; - max-width: 1100px; + max-width: 1600px; margin: 0 auto 1.5rem; display: flex; flex-wrap: wrap; @@ -374,7 +374,7 @@ .results { position: relative; z-index: 1; - max-width: 1100px; + max-width: 1600px; margin: 0 auto; } diff --git a/src/pages/leaderboard/index.module.css b/src/pages/leaderboard/index.module.css new file mode 100644 index 0000000..2ccf652 --- /dev/null +++ b/src/pages/leaderboard/index.module.css @@ -0,0 +1,251 @@ +.page { + position: relative; + overflow: visible; + padding: 3.5rem 1.5rem 4rem; + background: var(--agml-page-bg); + color: var(--agml-text); +} + +.hero { + position: relative; + z-index: 1; + max-width: 1400px; + margin: 0 auto 2.5rem; + padding: 2.5rem 2.5rem 2rem; + border-radius: 28px; + border: 1px solid var(--agml-border); + background: var(--agml-surface); + box-shadow: var(--agml-shadow-strong); +} + +.heroContent { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.heroTag { + text-transform: uppercase; + letter-spacing: 0.2em; + font-size: 0.75rem; + font-weight: 600; + color: var(--agml-muted); + margin: 0; +} + +.heroTitle { + font-size: clamp(2.2rem, 2.8vw, 3.4rem); + margin: 0; + color: var(--agml-text); +} + +.heroSubtitle { + margin: 0; + font-size: 1.05rem; + color: var(--agml-muted); + max-width: 720px; +} + +.controls { + position: relative; + z-index: 30; + max-width: 1400px; + margin: 0 auto 1.5rem; + display: flex; + flex-wrap: wrap; + gap: 1.2rem; + align-items: center; + justify-content: space-between; + padding: 1.5rem 2rem; + border-radius: 22px; + background: var(--agml-surface); + border: 1px solid var(--agml-border); + box-shadow: var(--agml-shadow); +} + +.dropdownRow { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.dropdown { + position: relative; + display: flex; + flex-direction: column; + gap: 0.4rem; + min-width: 200px; + z-index: 31; +} + +.dropdownLabel { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--agml-muted); +} + +.dropdownTrigger { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 14px; + border: 1px solid var(--agml-border-strong); + padding: 0.6rem 0.9rem; + background: var(--agml-surface-strong); + font-size: 0.92rem; + color: var(--agml-text); +} + +.dropdownChevron { + margin-left: 0.5rem; +} + +.dropdownMenu { + position: absolute; + top: calc(100% + 0.4rem); + left: 0; + right: 0; + border-radius: 16px; + border: 1px solid var(--agml-border); + background: var(--agml-surface-strong); + padding: 0.4rem 0; + max-height: 240px; + overflow: auto; + box-shadow: var(--agml-shadow); + z-index: 40; +} + +.dropdownEmpty { + padding: 0.55rem 0.9rem; + font-size: 0.85rem; + color: var(--agml-muted); +} + +.dropdownOption { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.9rem; + width: 100%; + text-align: left; + border: none; + background: transparent; + cursor: pointer; + font-size: 0.9rem; +} + +.dropdownOptionSelected { + background: var(--agml-accent-soft); + color: var(--ifm-color-primary); +} + +.dropdownCheckbox { + width: 1.2rem; + height: 1.2rem; + border-radius: 6px; + border: 1px solid var(--agml-border-strong); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + color: var(--ifm-color-primary); +} + +.clearButton { + border: 1px solid var(--agml-border-strong); + background: transparent; + border-radius: 999px; + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: var(--agml-text); + cursor: pointer; +} + +.results { + position: relative; + z-index: 1; + max-width: 1400px; + margin: 0 auto; +} + +.status { + color: var(--agml-muted); +} + +.leaderboardTable { + width: 100%; + border-collapse: collapse; + background: var(--agml-surface-strong); + border-radius: 20px; + overflow: hidden; + box-shadow: var(--agml-shadow); +} + +.leaderboardTable th, +.leaderboardTable td { + text-align: left; + padding: 0.85rem 1.1rem; + border-bottom: 1px solid var(--agml-border); + font-size: 0.92rem; + color: var(--agml-text); +} + +.leaderboardTable th { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--agml-muted); + background: var(--agml-surface); +} + +.leaderboardTable tbody tr:last-child td { + border-bottom: none; +} + +.datasetsCell { + color: var(--agml-muted); + font-size: 0.85rem; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-top: 1.5rem; +} + +.paginationButton { + border: 1px solid var(--agml-border-strong); + background: var(--agml-surface-strong); + border-radius: 999px; + padding: 0.5rem 1.1rem; + font-size: 0.85rem; + color: var(--agml-text); + cursor: pointer; +} + +.paginationButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.paginationStatus { + font-size: 0.82rem; + color: var(--agml-muted); +} + +html[data-theme='dark'] .dropdownOptionSelected { + color: var(--ifm-color-primary-lightest); +} + +@media (max-width: 996px) { + .hero { + padding: 2rem 1.6rem; + } + + .controls { + padding: 1.2rem 1.5rem; + } +} diff --git a/src/pages/leaderboard/index.tsx b/src/pages/leaderboard/index.tsx new file mode 100644 index 0000000..2b17265 --- /dev/null +++ b/src/pages/leaderboard/index.tsx @@ -0,0 +1,208 @@ +import { useEffect, useMemo, useState } from 'react'; +import Layout from '@theme/Layout'; +import { computeGlobalLeaderboard, useGlobalPerformance } from '../../lib/performance'; +import styles from './index.module.css'; + +const MIN_APPEARANCES = 3; +const PAGE_SIZE = 25; + +function toLabel(value: string) { + return value.replace(/_/g, ' '); +} + +function MultiSelectDropdown({ + label, + options, + selected, + onToggle, +}: { + label: string; + options: string[]; + selected: string[]; + onToggle: (value: string) => void; +}) { + const [open, setOpen] = useState(false); + + return ( +
+ + + {open && ( +
+ {options.length === 0 &&
No options
} + {options.map((option) => { + const isSelected = selected.includes(option); + return ( + + ); + })} +
+ )} +
+ ); +} + +export default function GlobalLeaderboardPage() { + const { data: records, loading, error } = useGlobalPerformance(); + + const [cropTypes, setCropTypes] = useState([]); + const [mlTasks, setMlTasks] = useState([]); + + const { cropTypeOptions, mlTaskOptions } = useMemo(() => { + const cropSet = new Set(); + const taskSet = new Set(); + for (const record of records) { + record.crop_types?.forEach((crop) => cropSet.add(crop)); + if (record.machine_learning_task) taskSet.add(record.machine_learning_task); + } + return { + cropTypeOptions: Array.from(cropSet).sort((a, b) => a.localeCompare(b)), + mlTaskOptions: Array.from(taskSet).sort((a, b) => a.localeCompare(b)), + }; + }, [records]); + + const toggleCropType = (value: string) => { + setCropTypes((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + }; + + const toggleMlTask = (value: string) => { + setMlTasks((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + }; + + const hasActiveFilters = cropTypes.length > 0 || mlTasks.length > 0; + const clearFilters = () => { + setCropTypes([]); + setMlTasks([]); + }; + + const leaderboard = useMemo( + () => computeGlobalLeaderboard(records, { cropTypes, mlTasks, minAppearances: MIN_APPEARANCES }), + [records, cropTypes, mlTasks] + ); + + const [page, setPage] = useState(1); + useEffect(() => setPage(1), [cropTypes, mlTasks]); + + const pageCount = Math.max(1, Math.ceil(leaderboard.length / PAGE_SIZE)); + const currentPage = Math.min(page, pageCount); + const pagedLeaderboard = useMemo( + () => leaderboard.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), + [leaderboard, currentPage] + ); + + return ( + +
+
+
+

AgML Model Leaderboard

+

Global model performance

+

+ Models are ranked by their average percentile across every dataset leaderboard they appear on. + Only models with at least {MIN_APPEARANCES} dataset appearances are included. +

+
+
+ +
+
+ + +
+ {hasActiveFilters && ( + + )} +
+ +
+ {loading &&

Loading leaderboard…

} + {error &&

Error: {error.message}

} + {!loading && !error && leaderboard.length === 0 && ( +

+ No models have at least {MIN_APPEARANCES} dataset appearances for the current filters. +

+ )} + {!loading && !error && leaderboard.length > 0 && ( + <> + + + + + + + + + + + + {pagedLeaderboard.map((entry, index) => ( + + + + + + + + ))} + +
RankModelAvg. percentileAppearancesDatasets
{(currentPage - 1) * PAGE_SIZE + index + 1}{entry.model}{entry.averagePercentile.toFixed(1)}{entry.appearances}{entry.datasets.map((name) => toLabel(name)).join(', ')}
+ {pageCount > 1 && ( +
+ + + Page {currentPage} of {pageCount} + + +
+ )} + + )} +
+
+
+ ); +} From f765452f14612d094380ed3d9976ddc9587fcdc3 Mon Sep 17 00:00:00 2001 From: Jared Smith Date: Mon, 6 Jul 2026 09:36:42 -0700 Subject: [PATCH 2/8] beginnings of leaderboard ui --- scripts/generate-datasets.mjs | 97 +++++++++++++++- src/components/DatasetMetadataModal.tsx | 5 +- src/lib/performance.ts | 142 +++++++++++++++++++++--- src/pages/leaderboard/index.tsx | 6 + 4 files changed, 231 insertions(+), 19 deletions(-) diff --git a/scripts/generate-datasets.mjs b/scripts/generate-datasets.mjs index 65cda1a..2e28e90 100644 --- a/scripts/generate-datasets.mjs +++ b/scripts/generate-datasets.mjs @@ -53,6 +53,96 @@ function buildDatasetMetadataLookup(...manifests) { return lookup; } +// Raw benchmark run records (see static/data/performance/.json) are converted into +// leaderboard rows here. This logic is mirrored in src/lib/performance.ts for client-side +// rendering of the same files — keep the two in sync if the run schema changes. +const TASK_METRIC_KEYS = { + classification: ['f1', 'accuracy', 'top1_accuracy'], + detection: ['map', 'map_50', 'map50', 'mAP', 'mAP@0.5'], + segmentation: ['miou', 'iou', 'mean_iou'], +}; + +function isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value); +} + +function resolveMetricKey(task, metrics) { + const candidates = TASK_METRIC_KEYS[task] ?? []; + for (const key of candidates) { + if (isFiniteNumber(metrics[key])) return key; + } + return Object.keys(metrics).find((key) => isFiniteNumber(metrics[key])) ?? null; +} + +function buildRunNote(entry) { + const parts = []; + if (isFiniteNumber(entry.num_samples)) parts.push(`${entry.num_samples} samples`); + if (typeof entry.device === 'string' && entry.device.trim()) parts.push(entry.device.trim()); + return parts.length ? parts.join(' · ') : null; +} + +function buildFinetuneNote(finetune) { + if (!finetune || typeof finetune !== 'object') return null; + const parts = []; + if (isFiniteNumber(finetune.epochs)) parts.push(`${finetune.epochs} epochs`); + if (isFiniteNumber(finetune.train_samples)) parts.push(`${finetune.train_samples} train samples`); + return parts.length ? parts.join(' · ') : null; +} + +function makeLeaderboardRow(entry, metricKey, variant) { + return { + model: entry.model.trim(), + score: entry.metrics[metricKey], + variant, + date: typeof entry.timestamp === 'string' ? entry.timestamp.slice(0, 10) : null, + submitted_by: null, + link: null, + notes: variant === 'fine-tuned' ? buildFinetuneNote(entry.finetune) : buildRunNote(entry), + }; +} + +// A model can appear multiple times per dataset (repeated runs, or a fine-tuned run alongside +// a zero-shot one). The leaderboard shows the best zero-shot result and the best fine-tuned +// result per model, side by side, rather than collapsing to a single row. +function buildLeaderboardFromRawResults(rawResults) { + const scored = []; + for (const entry of rawResults) { + if (!entry || typeof entry !== 'object') continue; + if (typeof entry.model !== 'string' || !entry.model.trim()) continue; + if (!entry.metrics || typeof entry.metrics !== 'object') continue; + const metricKey = resolveMetricKey(entry.task, entry.metrics); + if (!metricKey || !isFiniteNumber(entry.metrics[metricKey])) continue; + scored.push({ + entry, + metricKey, + score: entry.metrics[metricKey], + isFinetune: entry.finetune != null && typeof entry.finetune === 'object', + }); + } + + const byModel = new Map(); + for (const item of scored) { + const model = item.entry.model.trim(); + const group = byModel.get(model) ?? { zeroShot: null, fineTuned: null }; + const slot = item.isFinetune ? 'fineTuned' : 'zeroShot'; + if (!group[slot] || item.score > group[slot].score) group[slot] = item; + byModel.set(model, group); + } + + const rows = []; + for (const group of byModel.values()) { + if (group.zeroShot) rows.push(makeLeaderboardRow(group.zeroShot.entry, group.zeroShot.metricKey, 'zero-shot')); + if (group.fineTuned) rows.push(makeLeaderboardRow(group.fineTuned.entry, group.fineTuned.metricKey, 'fine-tuned')); + } + + rows.sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity)); + rows.forEach((row, index) => { + row.rank = index + 1; + }); + + return { metric: scored.length ? scored[0].metricKey : null, leaderboard: rows }; +} + function sortLeaderboardEntries(entries) { return [...entries].sort((a, b) => { const rankA = typeof a.rank === 'number' ? a.rank : null; @@ -69,7 +159,11 @@ function buildGlobalPerformanceRecords(performanceDatasets, metadataLookup) { const records = []; for (const datasetName of performanceDatasets) { const raw = readJson(path.join(performanceDir, `${datasetName}.json`)); - const leaderboard = Array.isArray(raw) ? raw : Array.isArray(raw?.leaderboard) ? raw.leaderboard : []; + const leaderboard = Array.isArray(raw) + ? buildLeaderboardFromRawResults(raw).leaderboard + : Array.isArray(raw?.leaderboard) + ? raw.leaderboard + : []; const entries = sortLeaderboardEntries(leaderboard.filter((entry) => typeof entry?.model === 'string' && entry.model.trim())); const total = entries.length; if (total === 0) continue; @@ -84,6 +178,7 @@ function buildGlobalPerformanceRecords(performanceDatasets, metadataLookup) { percentile, crop_types: meta.crop_types, machine_learning_task: meta.machine_learning_task, + variant: entry.variant === 'zero-shot' || entry.variant === 'fine-tuned' ? entry.variant : null, }); }); } diff --git a/src/components/DatasetMetadataModal.tsx b/src/components/DatasetMetadataModal.tsx index 1e67699..baa49af 100644 --- a/src/components/DatasetMetadataModal.tsx +++ b/src/components/DatasetMetadataModal.tsx @@ -212,13 +212,14 @@ export function DatasetMetadataModal({ Rank Model + Result type Score Submitted by {datasetPerformance.data.entries.map((entry, index) => ( - + {entry.rank ?? index + 1} {entry.link ? ( @@ -228,7 +229,9 @@ export function DatasetMetadataModal({ ) : ( entry.model )} + {entry.notes &&
{entry.notes}
} + {entry.variant === 'fine-tuned' ? 'Fine-tuned' : entry.variant === 'zero-shot' ? 'Zero-shot' : '—'} {entry.score != null ? entry.score.toLocaleString() : '—'} {entry.submitted_by ?? '—'} diff --git a/src/lib/performance.ts b/src/lib/performance.ts index b710d10..eb93768 100644 --- a/src/lib/performance.ts +++ b/src/lib/performance.ts @@ -9,6 +9,7 @@ export interface PerformanceEntry { date: string | null; link: string | null; notes: string | null; + variant: 'zero-shot' | 'fine-tuned' | null; } export interface DatasetPerformance { @@ -48,27 +49,123 @@ function normalizeEntry(raw: unknown): PerformanceEntry | null { date: toText(raw.date ?? raw.submitted_at), link: toText(raw.link ?? raw.url ?? raw.source_link), notes: toText(raw.notes), + variant: null, }; } -function normalizePerformance(json: unknown): DatasetPerformance { - const rawEntries = Array.isArray(json) - ? json - : isRecord(json) && Array.isArray(json.leaderboard) - ? json.leaderboard - : []; - const entries = rawEntries - .map(normalizeEntry) - .filter((entry): entry is PerformanceEntry => entry != null) - .sort((a, b) => { - if (a.rank != null && b.rank != null) return a.rank - b.rank; - if (a.score != null && b.score != null) return b.score - a.score; - return 0; +// Raw benchmark run records (see static/data/performance/.json) are converted into +// leaderboard rows here. This logic is mirrored in scripts/generate-datasets.mjs, which derives +// global.json from the same files at build time — keep the two in sync if the run schema changes. +const TASK_METRIC_KEYS: Record = { + classification: ['f1', 'accuracy', 'top1_accuracy'], + detection: ['map', 'map_50', 'map50', 'mAP', 'mAP@0.5'], + segmentation: ['miou', 'iou', 'mean_iou'], +}; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value); +} + +function resolveMetricKey(task: unknown, metrics: Record): string | null { + const candidates = typeof task === 'string' ? (TASK_METRIC_KEYS[task] ?? []) : []; + for (const key of candidates) { + if (isFiniteNumber(metrics[key])) return key; + } + return Object.keys(metrics).find((key) => isFiniteNumber(metrics[key])) ?? null; +} + +function buildRunNote(entry: Record): string | null { + const parts: string[] = []; + if (isFiniteNumber(entry.num_samples)) parts.push(`${entry.num_samples} samples`); + const device = toText(entry.device); + if (device) parts.push(device); + return parts.length ? parts.join(' · ') : null; +} + +function buildFinetuneNote(finetune: unknown): string | null { + if (!isRecord(finetune)) return null; + const parts: string[] = []; + if (isFiniteNumber(finetune.epochs)) parts.push(`${finetune.epochs} epochs`); + if (isFiniteNumber(finetune.train_samples)) parts.push(`${finetune.train_samples} train samples`); + return parts.length ? parts.join(' · ') : null; +} + +function makeLeaderboardRow( + entry: Record, + metricKey: string, + variant: 'zero-shot' | 'fine-tuned' +): Omit & { score: number } { + const metrics = entry.metrics as Record; + return { + model: (entry.model as string).trim(), + score: metrics[metricKey] as number, + variant, + date: toText(entry.timestamp)?.slice(0, 10) ?? null, + submitted_by: null, + link: null, + notes: variant === 'fine-tuned' ? buildFinetuneNote(entry.finetune) : buildRunNote(entry), + }; +} + +// A model can appear multiple times per dataset (repeated runs, or a fine-tuned run alongside a +// zero-shot one). The leaderboard shows the best zero-shot result and the best fine-tuned result +// per model, side by side, rather than collapsing to a single row. +function buildLeaderboardFromRawResults(rawResults: unknown[]): DatasetPerformance { + type ScoredEntry = { entry: Record; metricKey: string; score: number; isFinetune: boolean }; + const scored: ScoredEntry[] = []; + + for (const raw of rawResults) { + if (!isRecord(raw)) continue; + const model = toText(raw.model); + if (!model) continue; + if (!isRecord(raw.metrics)) continue; + const metricKey = resolveMetricKey(raw.task, raw.metrics); + if (!metricKey || !isFiniteNumber(raw.metrics[metricKey])) continue; + scored.push({ + entry: raw, + metricKey, + score: raw.metrics[metricKey] as number, + isFinetune: isRecord(raw.finetune), }); + } + + const byModel = new Map(); + for (const item of scored) { + const model = (item.entry.model as string).trim(); + const group = byModel.get(model) ?? { zeroShot: null, fineTuned: null }; + const slot = item.isFinetune ? 'fineTuned' : 'zeroShot'; + if (!group[slot] || item.score > group[slot]!.score) group[slot] = item; + byModel.set(model, group); + } + + const rows: Omit[] = []; + for (const group of byModel.values()) { + if (group.zeroShot) rows.push(makeLeaderboardRow(group.zeroShot.entry, group.zeroShot.metricKey, 'zero-shot')); + if (group.fineTuned) rows.push(makeLeaderboardRow(group.fineTuned.entry, group.fineTuned.metricKey, 'fine-tuned')); + } + + rows.sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity)); + const entries: PerformanceEntry[] = rows.map((row, index) => ({ ...row, rank: index + 1 })); - const metric = isRecord(json) ? toText(json.metric) : null; + return { metric: scored.length ? scored[0].metricKey : null, entries }; +} + +function normalizePerformance(json: unknown): DatasetPerformance { + if (Array.isArray(json)) return buildLeaderboardFromRawResults(json); - return { metric, entries }; + if (isRecord(json) && Array.isArray(json.leaderboard)) { + const entries = json.leaderboard + .map(normalizeEntry) + .filter((entry): entry is PerformanceEntry => entry != null) + .sort((a, b) => { + if (a.rank != null && b.rank != null) return a.rank - b.rank; + if (a.score != null && b.score != null) return b.score - a.score; + return 0; + }); + return { metric: toText(json.metric), entries }; + } + + return { metric: null, entries: [] }; } export interface GlobalPerformanceRecord { @@ -77,6 +174,7 @@ export interface GlobalPerformanceRecord { percentile: number; crop_types: string[] | null; machine_learning_task: string | null; + variant: 'zero-shot' | 'fine-tuned' | null; } export interface GlobalLeaderboardEntry { @@ -84,6 +182,7 @@ export interface GlobalLeaderboardEntry { averagePercentile: number; appearances: number; datasets: string[]; + fineTunedDatasets: string[]; } function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord | null { @@ -97,12 +196,15 @@ function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord ? raw.crop_types.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) : null; + const variant = raw.variant === 'zero-shot' || raw.variant === 'fine-tuned' ? raw.variant : null; + return { model, dataset, percentile, crop_types: cropTypes?.length ? cropTypes : null, machine_learning_task: toText(raw.machine_learning_task), + variant, }; } @@ -111,16 +213,21 @@ export function computeGlobalLeaderboard( options: { cropTypes?: string[]; mlTasks?: string[]; minAppearances?: number } = {} ): GlobalLeaderboardEntry[] { const { cropTypes = [], mlTasks = [], minAppearances = 3 } = options; - const stats = new Map }>(); + const stats = new Map< + string, + { totalPercentile: number; appearances: number; datasets: Set; fineTunedDatasets: Set } + >(); for (const record of records) { if (cropTypes.length && !record.crop_types?.some((crop) => cropTypes.includes(crop))) continue; if (mlTasks.length && !(record.machine_learning_task && mlTasks.includes(record.machine_learning_task))) continue; - const entryStats = stats.get(record.model) ?? { totalPercentile: 0, appearances: 0, datasets: new Set() }; + const entryStats = + stats.get(record.model) ?? { totalPercentile: 0, appearances: 0, datasets: new Set(), fineTunedDatasets: new Set() }; entryStats.totalPercentile += record.percentile; entryStats.appearances += 1; entryStats.datasets.add(record.dataset); + if (record.variant === 'fine-tuned') entryStats.fineTunedDatasets.add(record.dataset); stats.set(record.model, entryStats); } @@ -131,6 +238,7 @@ export function computeGlobalLeaderboard( averagePercentile: entryStats.totalPercentile / entryStats.appearances, appearances: entryStats.appearances, datasets: Array.from(entryStats.datasets).sort(), + fineTunedDatasets: Array.from(entryStats.fineTunedDatasets).sort(), })) .sort((a, b) => b.averagePercentile - a.averagePercentile); } diff --git a/src/pages/leaderboard/index.tsx b/src/pages/leaderboard/index.tsx index 2b17265..b9b19de 100644 --- a/src/pages/leaderboard/index.tsx +++ b/src/pages/leaderboard/index.tsx @@ -161,6 +161,7 @@ export default function GlobalLeaderboardPage() { Model Avg. percentile Appearances + Fine-tuned Datasets @@ -171,6 +172,11 @@ export default function GlobalLeaderboardPage() { {entry.model} {entry.averagePercentile.toFixed(1)} {entry.appearances} + + {entry.fineTunedDatasets.length > 0 + ? `${entry.fineTunedDatasets.length} of ${entry.datasets.length}` + : '—'} + {entry.datasets.map((name) => toLabel(name)).join(', ')} ))} From 8d4d418284714964e659a1ea505493c45dc62ccd Mon Sep 17 00:00:00 2001 From: Jared Smith Date: Tue, 14 Jul 2026 09:27:08 -0700 Subject: [PATCH 3/8] updating leaderboard uis --- scripts/generate-datasets.mjs | 7 +- .../DatasetMetadataModal.module.css | 118 ++++++- src/components/DatasetMetadataModal.tsx | 304 ++++++++++++++++-- src/components/MultiSelectDropdown.module.css | 86 +++++ src/components/MultiSelectDropdown.tsx | 65 ++++ src/lib/performance.ts | 255 +++++++++++++-- src/pages/leaderboard/index.module.css | 135 +++----- src/pages/leaderboard/index.tsx | 199 +++++++----- 8 files changed, 940 insertions(+), 229 deletions(-) create mode 100644 src/components/MultiSelectDropdown.module.css create mode 100644 src/components/MultiSelectDropdown.tsx diff --git a/scripts/generate-datasets.mjs b/scripts/generate-datasets.mjs index 2e28e90..1b3e45c 100644 --- a/scripts/generate-datasets.mjs +++ b/scripts/generate-datasets.mjs @@ -90,6 +90,7 @@ function buildFinetuneNote(finetune) { } function makeLeaderboardRow(entry, metricKey, variant) { + const finetune = entry.finetune; return { model: entry.model.trim(), score: entry.metrics[metricKey], @@ -97,7 +98,9 @@ function makeLeaderboardRow(entry, metricKey, variant) { date: typeof entry.timestamp === 'string' ? entry.timestamp.slice(0, 10) : null, submitted_by: null, link: null, - notes: variant === 'fine-tuned' ? buildFinetuneNote(entry.finetune) : buildRunNote(entry), + notes: variant === 'fine-tuned' ? buildFinetuneNote(finetune) : buildRunNote(entry), + optimized: Boolean(entry.optimized) || (finetune != null && typeof finetune === 'object' && Boolean(finetune.optimized)), + platform: typeof entry.device === 'string' && entry.device.trim() ? entry.device.trim() : null, }; } @@ -179,6 +182,8 @@ function buildGlobalPerformanceRecords(performanceDatasets, metadataLookup) { crop_types: meta.crop_types, machine_learning_task: meta.machine_learning_task, variant: entry.variant === 'zero-shot' || entry.variant === 'fine-tuned' ? entry.variant : null, + optimized: Boolean(entry.optimized), + platform: typeof entry.platform === 'string' && entry.platform.trim() ? entry.platform.trim() : null, }); }); } diff --git a/src/components/DatasetMetadataModal.module.css b/src/components/DatasetMetadataModal.module.css index 42a81a6..fa450c8 100644 --- a/src/components/DatasetMetadataModal.module.css +++ b/src/components/DatasetMetadataModal.module.css @@ -161,7 +161,8 @@ .exampleImage { display: block; width: 100%; - height: auto; + height: 65vh; + object-fit: contain; border-radius: 18px; border: 1px solid var(--agml-border); background: var(--agml-surface-strong); @@ -215,8 +216,65 @@ overflow-x: auto; } +.filterBar { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 1rem; + margin: 0.75rem 0 1rem; + padding: 0.9rem 1rem; + border-radius: 16px; + border: 1px solid var(--agml-border); + background: var(--agml-surface); +} + +.filterRange { + display: flex; + flex-direction: column; + gap: 0.4rem; + min-width: 160px; +} + +.filterRangeLabel { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--agml-muted); +} + +.filterRangeInputs { + display: flex; + align-items: center; + gap: 0.4rem; + color: var(--agml-muted); +} + +.filterRangeInput { + width: 100%; + min-width: 0; + border-radius: 10px; + border: 1px solid var(--agml-border-strong); + padding: 0.5rem 0.6rem; + background: var(--agml-surface-strong); + color: var(--agml-text); + font-size: 0.88rem; +} + +.clearFiltersButton { + border: 1px solid var(--agml-border-strong); + background: transparent; + border-radius: 999px; + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: var(--agml-text); + cursor: pointer; + white-space: nowrap; +} + .leaderboardTable { width: 100%; + max-width: 100%; + table-layout: fixed; margin-top: 0.6rem; border-collapse: collapse; font-size: 0.88rem; @@ -227,7 +285,63 @@ padding: 0.55rem 0.7rem; text-align: left; border-bottom: 1px solid var(--agml-border); - white-space: nowrap; + white-space: normal; + overflow-wrap: anywhere; +} + +.colModel { + width: 18%; +} + +.colResultType { + width: 12%; +} + +.colSplit { + width: 13%; +} + +.colConfig { + width: 12%; +} + +.colMetric { + width: 22%; +} + +.colTime { + width: 15%; +} + +.colPlatform { + width: 8%; +} + +.clickableRow { + cursor: pointer; +} + +.clickableRow:hover { + background: var(--agml-surface); +} + +.expandChevron { + display: inline-block; + width: 1em; + margin-right: 0.35rem; + color: var(--agml-muted); + font-size: 0.8rem; +} + +.notesCell { + background: var(--agml-surface); +} + +.notesText { + margin: 0; + color: var(--agml-text); + line-height: 1.5; + overflow-wrap: anywhere; } .leaderboardTable th { diff --git a/src/components/DatasetMetadataModal.tsx b/src/components/DatasetMetadataModal.tsx index baa49af..e5e70f6 100644 --- a/src/components/DatasetMetadataModal.tsx +++ b/src/components/DatasetMetadataModal.tsx @@ -1,7 +1,9 @@ -import { useEffect } from 'react'; +import { Fragment, useEffect, useMemo, useState } from 'react'; import type { Dataset } from '../lib/datasets'; import { formatDisplayLocation } from '../lib/datasets'; -import { useDatasetPerformance } from '../lib/performance'; +import { classifyMetricLabel, METRIC_CATEGORY_LABELS, useDatasetPerformance } from '../lib/performance'; +import type { MetricCategory, PerformanceEntry } from '../lib/performance'; +import { MultiSelectDropdown } from './MultiSelectDropdown'; import styles from './DatasetMetadataModal.module.css'; function formatImageCount(count: number | null) { @@ -47,6 +49,49 @@ function hasExampleImage(url: string | null): url is string { return Boolean(url); } +function formatResultType(entry: { variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean }) { + const base = entry.variant === 'fine-tuned' ? 'Fine-tuned' : entry.variant === 'zero-shot' ? 'Zero-shot' : '—'; + if (base === '—') return base; + return entry.optimized ? `${base} (optimized)` : base; +} + +function resultTypeKey(entry: { variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean }): string | null { + if (!entry.variant) return null; + return entry.optimized ? `${entry.variant}-optimized` : entry.variant; +} + +function formatResultTypeKey(key: string) { + const optimized = key.endsWith('-optimized'); + const base = optimized ? key.slice(0, -'-optimized'.length) : key; + const label = base === 'fine-tuned' ? 'Fine-tuned' : 'Zero-shot'; + return optimized ? `${label} (optimized)` : label; +} + +function parseFilterNumber(value: string): number | null { + if (!value.trim()) return null; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function formatTimePerImage(value: number | null) { + if (value == null || !Number.isFinite(value)) return '—'; + return `${value.toFixed(3)} s/img`; +} + +function formatMetricScore(entry: { metrics: { key: string; label: string; value: number }[]; score: number | null }) { + if (entry.metrics.length > 0) { + return entry.metrics.map((metric) => `${metric.label}: ${metric.value.toFixed(3)}`).join(' · '); + } + return entry.score != null ? entry.score.toLocaleString() : '—'; +} + +function formatTrainInfTime(entry: { trainTimePerImage: number | null; infTimePerImage: number | null }) { + const train = entry.trainTimePerImage != null ? `train ${formatTimePerImage(entry.trainTimePerImage)}` : null; + const inf = entry.infTimePerImage != null ? `inf ${formatTimePerImage(entry.infTimePerImage)}` : null; + const parts = [train, inf].filter((value): value is string => value != null); + return parts.length ? parts.join(' / ') : '—'; +} + function formatLoaderInstructions(dataset: Dataset) { if (dataset.source === 'huggingface') { return { @@ -91,6 +136,90 @@ export function DatasetMetadataModal({ const datasetPerformance = useDatasetPerformance(open ? (dataset?.name ?? null) : null); + const [metricTypeFilter, setMetricTypeFilter] = useState([]); + const [resultTypeFilter, setResultTypeFilter] = useState([]); + const [platformFilter, setPlatformFilter] = useState([]); + const [trainingTimeMin, setTrainingTimeMin] = useState(''); + const [trainingTimeMax, setTrainingTimeMax] = useState(''); + const [trainingSizeMin, setTrainingSizeMin] = useState(''); + const [trainingSizeMax, setTrainingSizeMax] = useState(''); + + const allEntries = datasetPerformance.data?.entries ?? []; + + const { metricTypeOptions, resultTypeOptions, platformOptions } = useMemo(() => { + const metricTypes = new Set(); + const resultTypes = new Set(); + const platforms = new Set(); + for (const entry of allEntries) { + entry.metricCategories.forEach((category) => metricTypes.add(category)); + const key = resultTypeKey(entry); + if (key) resultTypes.add(key); + if (entry.platform) platforms.add(entry.platform); + } + return { + metricTypeOptions: Array.from(metricTypes), + resultTypeOptions: Array.from(resultTypes).sort(), + platformOptions: Array.from(platforms).sort(), + }; + }, [allEntries]); + + const toggleFilter = (setter: (updater: (current: string[]) => string[]) => void, value: string) => { + setter((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + }; + + const filteredEntries = useMemo(() => { + const timeMin = parseFilterNumber(trainingTimeMin); + const timeMax = parseFilterNumber(trainingTimeMax); + const sizeMin = parseFilterNumber(trainingSizeMin); + const sizeMax = parseFilterNumber(trainingSizeMax); + + return allEntries.filter((entry: PerformanceEntry) => { + if (metricTypeFilter.length && !entry.metricCategories.some((category) => metricTypeFilter.includes(category))) { + return false; + } + if (resultTypeFilter.length) { + const key = resultTypeKey(entry); + if (!key || !resultTypeFilter.includes(key)) return false; + } + if (platformFilter.length && !(entry.platform && platformFilter.includes(entry.platform))) return false; + if (timeMin != null && (entry.trainTimePerImage == null || entry.trainTimePerImage < timeMin)) return false; + if (timeMax != null && (entry.trainTimePerImage == null || entry.trainTimePerImage > timeMax)) return false; + if (sizeMin != null && (entry.trainPercentage == null || entry.trainPercentage < sizeMin)) return false; + if (sizeMax != null && (entry.trainPercentage == null || entry.trainPercentage > sizeMax)) return false; + return true; + }); + }, [allEntries, metricTypeFilter, resultTypeFilter, platformFilter, trainingTimeMin, trainingTimeMax, trainingSizeMin, trainingSizeMax]); + + const hasActiveLeaderboardFilters = + metricTypeFilter.length > 0 || + resultTypeFilter.length > 0 || + platformFilter.length > 0 || + trainingTimeMin !== '' || + trainingTimeMax !== '' || + trainingSizeMin !== '' || + trainingSizeMax !== ''; + + const clearLeaderboardFilters = () => { + setMetricTypeFilter([]); + setResultTypeFilter([]); + setPlatformFilter([]); + setTrainingTimeMin(''); + setTrainingTimeMax(''); + setTrainingSizeMin(''); + setTrainingSizeMax(''); + }; + + const [expandedRowKeys, setExpandedRowKeys] = useState>(new Set()); + const toggleExpandedRow = (key: string) => { + setExpandedRowKeys((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + useEffect(() => setExpandedRowKeys(new Set()), [filteredEntries]); + if (!open || dataset == null) return null; const detailRows = [ @@ -207,37 +336,148 @@ export function DatasetMetadataModal({ Metric: {datasetPerformance.data.metric}

)} - - - - - - - - - - - - {datasetPerformance.data.entries.map((entry, index) => ( - - - - - - + +
+ toggleFilter(setMetricTypeFilter, value)} + formatOption={(value) => METRIC_CATEGORY_LABELS[value as MetricCategory]} + /> + toggleFilter(setResultTypeFilter, value)} + formatOption={formatResultTypeKey} + /> + toggleFilter(setPlatformFilter, value)} + /> +
+ +
+ setTrainingTimeMin(event.target.value)} + className={styles.filterRangeInput} + /> + + setTrainingTimeMax(event.target.value)} + className={styles.filterRangeInput} + /> +
+
+
+ +
+ setTrainingSizeMin(event.target.value)} + className={styles.filterRangeInput} + /> + + setTrainingSizeMax(event.target.value)} + className={styles.filterRangeInput} + /> +
+
+ {hasActiveLeaderboardFilters && ( + + )} +
+ + {filteredEntries.length === 0 ? ( +

No results match the selected filters.

+ ) : ( +
RankModelResult typeScoreSubmitted by
{entry.rank ?? index + 1} - {entry.link ? ( - - {entry.model} - - ) : ( - entry.model - )} - {entry.notes &&
{entry.notes}
} -
{entry.variant === 'fine-tuned' ? 'Fine-tuned' : entry.variant === 'zero-shot' ? 'Zero-shot' : '—'}{entry.score != null ? entry.score.toLocaleString() : '—'}{entry.submitted_by ?? '—'}
+ + + + + + + + + + + + + + + + + + - ))} - -
Model nameResult typeTrain / test / val images (%)Train / test configMetric scoreNormalized train / inf time (s/img)Platform
+ + + {filteredEntries.map((entry, index) => { + const rowKey = `${entry.model}-${entry.variant ?? 'default'}-${index}`; + const isExpanded = expandedRowKeys.has(rowKey); + return ( + + toggleExpandedRow(rowKey)} + aria-expanded={isExpanded} + > + + + {isExpanded ? '▾' : '▸'} + + {entry.link ? ( + event.stopPropagation()} + > + {entry.model} + + ) : ( + entry.model + )} + + {formatResultType(entry)} + {entry.splitBreakdown ?? '—'} + {entry.datasetConfig ?? '—'} + {formatMetricScore(entry)} + {formatTrainInfTime(entry)} + {entry.platform ?? '—'} + + {isExpanded && ( + + +

Necessary notes

+

{entry.notes ?? 'No additional notes for this result.'}

+ + + )} +
+ ); + })} + + + )} ) : (

No leaderboard results have been submitted for this dataset yet.

diff --git a/src/components/MultiSelectDropdown.module.css b/src/components/MultiSelectDropdown.module.css new file mode 100644 index 0000000..9d0fc56 --- /dev/null +++ b/src/components/MultiSelectDropdown.module.css @@ -0,0 +1,86 @@ +.dropdown { + position: relative; + display: flex; + flex-direction: column; + gap: 0.4rem; + min-width: 200px; + z-index: 31; +} + +.dropdownLabel { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--agml-muted); +} + +.dropdownTrigger { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 14px; + border: 1px solid var(--agml-border-strong); + padding: 0.6rem 0.9rem; + background: var(--agml-surface-strong); + font-size: 0.92rem; + color: var(--agml-text); +} + +.dropdownChevron { + margin-left: 0.5rem; +} + +.dropdownMenu { + position: absolute; + top: calc(100% + 0.4rem); + left: 0; + right: 0; + border-radius: 16px; + border: 1px solid var(--agml-border); + background: var(--agml-surface-strong); + padding: 0.4rem 0; + max-height: 240px; + overflow: auto; + box-shadow: var(--agml-shadow); + z-index: 40; +} + +.dropdownEmpty { + padding: 0.55rem 0.9rem; + font-size: 0.85rem; + color: var(--agml-muted); +} + +.dropdownOption { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.9rem; + width: 100%; + text-align: left; + border: none; + background: transparent; + cursor: pointer; + font-size: 0.9rem; +} + +.dropdownOptionSelected { + background: var(--agml-accent-soft); + color: var(--ifm-color-primary); +} + +.dropdownCheckbox { + width: 1.2rem; + height: 1.2rem; + border-radius: 6px; + border: 1px solid var(--agml-border-strong); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + color: var(--ifm-color-primary); +} + +:global(html[data-theme='dark']) .dropdownOptionSelected { + color: var(--ifm-color-primary-lightest); +} diff --git a/src/components/MultiSelectDropdown.tsx b/src/components/MultiSelectDropdown.tsx new file mode 100644 index 0000000..eb35da4 --- /dev/null +++ b/src/components/MultiSelectDropdown.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import styles from './MultiSelectDropdown.module.css'; + +export function MultiSelectDropdown({ + label, + options, + selected, + onToggle, + formatOption, +}: { + label: string; + options: string[]; + selected: string[]; + onToggle: (value: string) => void; + formatOption?: (value: string) => string; +}) { + const [open, setOpen] = useState(false); + const format = formatOption ?? ((value: string) => value); + + return ( +
+ + + {open && ( +
+ {options.length === 0 &&
No options
} + {options.map((option) => { + const isSelected = selected.includes(option); + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/src/lib/performance.ts b/src/lib/performance.ts index eb93768..83f0796 100644 --- a/src/lib/performance.ts +++ b/src/lib/performance.ts @@ -10,8 +10,39 @@ export interface PerformanceEntry { link: string | null; notes: string | null; variant: 'zero-shot' | 'fine-tuned' | null; + optimized: boolean; + splitBreakdown: string | null; + trainPercentage: number | null; + datasetConfig: string | null; + trainTimePerImage: number | null; + infTimePerImage: number | null; + platform: string | null; + metrics: MetricValue[]; + metricCategories: MetricCategory[]; } +export type MetricCategory = 'f1' | 'map' | 'precision_recall' | 'other'; + +export interface MetricValue { + key: string; + label: string; + value: number; +} + +export function classifyMetricLabel(label: string): MetricCategory { + if (label === 'F1') return 'f1'; + if (label.startsWith('mAP')) return 'map'; + if (label === 'Precision' || label === 'Recall') return 'precision_recall'; + return 'other'; +} + +export const METRIC_CATEGORY_LABELS: Record = { + f1: 'F1', + map: 'mAP', + precision_recall: 'Precision / Recall', + other: 'Other', +}; + export interface DatasetPerformance { metric: string | null; entries: PerformanceEntry[]; @@ -50,6 +81,15 @@ function normalizeEntry(raw: unknown): PerformanceEntry | null { link: toText(raw.link ?? raw.url ?? raw.source_link), notes: toText(raw.notes), variant: null, + optimized: Boolean(raw.optimized), + splitBreakdown: toText(raw.split_breakdown), + trainPercentage: toNumber(raw.train_percentage), + datasetConfig: toText(raw.dataset_config ?? raw.config), + trainTimePerImage: toNumber(raw.train_time_per_image), + infTimePerImage: toNumber(raw.inference_time_per_image), + platform: toText(raw.platform ?? raw.device), + metrics: [], + metricCategories: [], }; } @@ -74,20 +114,99 @@ function resolveMetricKey(task: unknown, metrics: Record): stri return Object.keys(metrics).find((key) => isFiniteNumber(metrics[key])) ?? null; } +// Every dataset can report F1, mAP, and precision/recall side by side, regardless of the +// dataset's primary task — resolveMetricKey above still picks one metric to rank/sort by, but +// this collects every recognized metric present on a run for display purposes. Detection runs in +// particular may report mAP at several IoU thresholds (map_50, map_75, map_50_95, ...); each is +// surfaced as its own labeled value rather than collapsed into one number. +const NAMED_METRIC_LABELS: Record = { + f1: 'F1', + accuracy: 'Accuracy', + top1_accuracy: 'Top-1 Accuracy', + precision: 'Precision', + prec: 'Precision', + recall: 'Recall', + rec: 'Recall', + miou: 'mIoU', + iou: 'IoU', + mean_iou: 'mIoU', +}; + +function isMapMetricKey(key: string): boolean { + return /^m?ap([_@-]|$)/i.test(key); +} + +function formatMapMetricLabel(key: string): string { + const normalized = key.toLowerCase(); + if (normalized === 'map' || normalized === 'map50' || normalized === 'map_50' || normalized === 'mAP@0.5'.toLowerCase()) { + return 'mAP@0.50'; + } + const match = normalized.match(/(\d{2,3})(?:[_-](\d{2,3}))?\s*$/); + if (!match) return 'mAP'; + const lo = (Number(match[1]) / 100).toFixed(2); + if (match[2]) { + const hi = (Number(match[2]) / 100).toFixed(2); + return `mAP@[${lo}:${hi}]`; + } + return `mAP@${lo}`; +} + +function labelForMetricKey(key: string): string | null { + const normalized = key.toLowerCase(); + if (isMapMetricKey(normalized)) return formatMapMetricLabel(normalized); + return NAMED_METRIC_LABELS[normalized] ?? null; +} + +function collectMetrics(metrics: Record): MetricValue[] { + const results: MetricValue[] = []; + for (const [key, value] of Object.entries(metrics)) { + if (!isFiniteNumber(value)) continue; + const label = labelForMetricKey(key); + if (!label) continue; + results.push({ key, label, value }); + } + return results; +} + function buildRunNote(entry: Record): string | null { - const parts: string[] = []; - if (isFiniteNumber(entry.num_samples)) parts.push(`${entry.num_samples} samples`); - const device = toText(entry.device); - if (device) parts.push(device); - return parts.length ? parts.join(' · ') : null; + const parts: string[] = ['Zero-shot']; + if (isFiniteNumber(entry.num_samples)) parts.push(`evaluated on ${entry.num_samples} images`); + return parts.join(' · '); } function buildFinetuneNote(finetune: unknown): string | null { if (!isRecord(finetune)) return null; - const parts: string[] = []; + const parts: string[] = ['Fine-tuned']; + if (isFiniteNumber(finetune.train_samples)) parts.push(`trained on ${finetune.train_samples} images from this dataset`); + if (isFiniteNumber(finetune.val_samples)) parts.push(`validated on ${finetune.val_samples} images`); if (isFiniteNumber(finetune.epochs)) parts.push(`${finetune.epochs} epochs`); - if (isFiniteNumber(finetune.train_samples)) parts.push(`${finetune.train_samples} train samples`); - return parts.length ? parts.join(' · ') : null; + if (isFiniteNumber(finetune.lr)) parts.push(`lr=${finetune.lr}`); + if (isFiniteNumber(finetune.weight_decay)) parts.push(`weight decay=${finetune.weight_decay}`); + if (isFiniteNumber(finetune.split_seed)) parts.push(`seed=${finetune.split_seed}`); + if (isFiniteNumber(finetune.train_ratio)) parts.push(`train ratio=${finetune.train_ratio}`); + return parts.join(' · '); +} + +function computeTrainPercentage(entry: Record, finetune: unknown): number | null { + const testSamples = isFiniteNumber(entry.num_samples) ? entry.num_samples : null; + const trainSamples = isRecord(finetune) && isFiniteNumber(finetune.train_samples) ? finetune.train_samples : null; + const valSamples = isRecord(finetune) && isFiniteNumber(finetune.val_samples) ? finetune.val_samples : null; + const total = (trainSamples ?? 0) + (testSamples ?? 0) + (valSamples ?? 0); + if (!total || trainSamples == null) return null; + return (trainSamples / total) * 100; +} + +// Rendered as train/test/val percentages in that fixed order, e.g. "10/80/10" or "-/100/-" when +// a split is absent — compact enough to fit the leaderboard's narrow split column. +function buildSplitBreakdown(entry: Record, finetune: unknown): string | null { + const testSamples = isFiniteNumber(entry.num_samples) ? entry.num_samples : null; + const trainSamples = isRecord(finetune) && isFiniteNumber(finetune.train_samples) ? finetune.train_samples : null; + const valSamples = isRecord(finetune) && isFiniteNumber(finetune.val_samples) ? finetune.val_samples : null; + const total = (trainSamples ?? 0) + (testSamples ?? 0) + (valSamples ?? 0); + if (!total) return null; + + const toShare = (value: number | null) => (value != null ? ((value / total) * 100).toFixed(0) : '-'); + return `${toShare(trainSamples)}/${toShare(testSamples)}/${toShare(valSamples)}`; } function makeLeaderboardRow( @@ -96,6 +215,11 @@ function makeLeaderboardRow( variant: 'zero-shot' | 'fine-tuned' ): Omit & { score: number } { const metrics = entry.metrics as Record; + const finetune = entry.finetune; + const trainSamples = isRecord(finetune) && isFiniteNumber(finetune.train_samples) ? finetune.train_samples : null; + const trainingTimeSeconds = isRecord(finetune) && isFiniteNumber(finetune.training_time_seconds) ? finetune.training_time_seconds : null; + const metricValues = collectMetrics(metrics); + return { model: (entry.model as string).trim(), score: metrics[metricKey] as number, @@ -103,7 +227,18 @@ function makeLeaderboardRow( date: toText(entry.timestamp)?.slice(0, 10) ?? null, submitted_by: null, link: null, - notes: variant === 'fine-tuned' ? buildFinetuneNote(entry.finetune) : buildRunNote(entry), + notes: variant === 'fine-tuned' ? buildFinetuneNote(finetune) : buildRunNote(entry), + optimized: Boolean(entry.optimized) || (isRecord(finetune) && Boolean(finetune.optimized)), + splitBreakdown: buildSplitBreakdown(entry, finetune), + trainPercentage: computeTrainPercentage(entry, finetune), + datasetConfig: toText(entry.dataset_config) ?? toText(entry.split), + trainTimePerImage: trainingTimeSeconds != null && trainSamples ? trainingTimeSeconds / trainSamples : null, + infTimePerImage: isFiniteNumber(entry.inference_time_seconds) && isFiniteNumber(entry.num_samples) && entry.num_samples > 0 + ? entry.inference_time_seconds / entry.num_samples + : null, + platform: toText(entry.device), + metrics: metricValues, + metricCategories: Array.from(new Set(metricValues.map((metric) => classifyMetricLabel(metric.label)))), }; } @@ -175,14 +310,40 @@ export interface GlobalPerformanceRecord { crop_types: string[] | null; machine_learning_task: string | null; variant: 'zero-shot' | 'fine-tuned' | null; + optimized: boolean; + platform: string | null; +} + +export interface GlobalLeaderboardDatasetDetail { + dataset: string; + percentile: number; + variant: 'zero-shot' | 'fine-tuned' | null; + optimized: boolean; + platform: string | null; +} + +export function globalResultTypeKey(record: { variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean }): string | null { + if (!record.variant) return null; + return record.optimized ? `${record.variant}-optimized` : record.variant; +} + +export function formatGlobalResultTypeKey(key: string) { + const optimized = key.endsWith('-optimized'); + const base = optimized ? key.slice(0, -'-optimized'.length) : key; + const label = base === 'fine-tuned' ? 'Fine-tuned' : 'Zero-shot'; + return optimized ? `${label} (optimized)` : label; } export interface GlobalLeaderboardEntry { model: string; + machineLearningTask: string | null; averagePercentile: number; appearances: number; datasets: string[]; fineTunedDatasets: string[]; + resultType: string; + optimized: boolean; + datasetDetails: GlobalLeaderboardDatasetDetail[]; } function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord | null { @@ -205,40 +366,98 @@ function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord crop_types: cropTypes?.length ? cropTypes : null, machine_learning_task: toText(raw.machine_learning_task), variant, + optimized: Boolean(raw.optimized), + platform: toText(raw.platform), }; } +function formatResultTypeLabel(variants: Set<'zero-shot' | 'fine-tuned'>, optimized: boolean) { + let base: string; + if (variants.size === 0) base = '—'; + else if (variants.size > 1) base = 'Mixed'; + else base = variants.has('fine-tuned') ? 'Fine-tuned' : 'Zero-shot'; + + if (base === '—') return base; + return optimized ? `${base} (optimized)` : base; +} + export function computeGlobalLeaderboard( records: GlobalPerformanceRecord[], - options: { cropTypes?: string[]; mlTasks?: string[]; minAppearances?: number } = {} + options: { + cropTypes?: string[]; + mlTasks?: string[]; + resultTypes?: string[]; + platforms?: string[]; + minAppearances?: number; + } = {} ): GlobalLeaderboardEntry[] { - const { cropTypes = [], mlTasks = [], minAppearances = 3 } = options; + const { cropTypes = [], mlTasks = [], resultTypes = [], platforms = [], minAppearances = 3 } = options; const stats = new Map< string, - { totalPercentile: number; appearances: number; datasets: Set; fineTunedDatasets: Set } + { + model: string; + machineLearningTask: string | null; + totalPercentile: number; + appearances: number; + datasets: Set; + fineTunedDatasets: Set; + variants: Set<'zero-shot' | 'fine-tuned'>; + optimized: boolean; + datasetDetails: GlobalLeaderboardDatasetDetail[]; + } >(); for (const record of records) { if (cropTypes.length && !record.crop_types?.some((crop) => cropTypes.includes(crop))) continue; if (mlTasks.length && !(record.machine_learning_task && mlTasks.includes(record.machine_learning_task))) continue; + if (resultTypes.length) { + const resultTypeKey = globalResultTypeKey(record); + if (!resultTypeKey || !resultTypes.includes(resultTypeKey)) continue; + } + if (platforms.length && !(record.platform && platforms.includes(record.platform))) continue; + const key = `${record.model}|||${record.machine_learning_task ?? ''}`; const entryStats = - stats.get(record.model) ?? { totalPercentile: 0, appearances: 0, datasets: new Set(), fineTunedDatasets: new Set() }; + stats.get(key) ?? + { + model: record.model, + machineLearningTask: record.machine_learning_task, + totalPercentile: 0, + appearances: 0, + datasets: new Set(), + fineTunedDatasets: new Set(), + variants: new Set<'zero-shot' | 'fine-tuned'>(), + optimized: false, + datasetDetails: [] as GlobalLeaderboardDatasetDetail[], + }; entryStats.totalPercentile += record.percentile; entryStats.appearances += 1; entryStats.datasets.add(record.dataset); if (record.variant === 'fine-tuned') entryStats.fineTunedDatasets.add(record.dataset); - stats.set(record.model, entryStats); + if (record.variant) entryStats.variants.add(record.variant); + if (record.optimized) entryStats.optimized = true; + entryStats.datasetDetails.push({ + dataset: record.dataset, + percentile: record.percentile, + variant: record.variant, + optimized: record.optimized, + platform: record.platform, + }); + stats.set(key, entryStats); } - return Array.from(stats.entries()) - .filter(([, entryStats]) => entryStats.appearances >= minAppearances) - .map(([model, entryStats]) => ({ - model, + return Array.from(stats.values()) + .filter((entryStats) => entryStats.appearances >= minAppearances) + .map((entryStats) => ({ + model: entryStats.model, + machineLearningTask: entryStats.machineLearningTask, averagePercentile: entryStats.totalPercentile / entryStats.appearances, appearances: entryStats.appearances, datasets: Array.from(entryStats.datasets).sort(), fineTunedDatasets: Array.from(entryStats.fineTunedDatasets).sort(), + resultType: formatResultTypeLabel(entryStats.variants, entryStats.optimized), + optimized: entryStats.optimized, + datasetDetails: entryStats.datasetDetails.sort((a, b) => a.dataset.localeCompare(b.dataset)), })) .sort((a, b) => b.averagePercentile - a.averagePercentile); } diff --git a/src/pages/leaderboard/index.module.css b/src/pages/leaderboard/index.module.css index 2ccf652..a207203 100644 --- a/src/pages/leaderboard/index.module.css +++ b/src/pages/leaderboard/index.module.css @@ -69,89 +69,6 @@ flex-wrap: wrap; } -.dropdown { - position: relative; - display: flex; - flex-direction: column; - gap: 0.4rem; - min-width: 200px; - z-index: 31; -} - -.dropdownLabel { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--agml-muted); -} - -.dropdownTrigger { - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 14px; - border: 1px solid var(--agml-border-strong); - padding: 0.6rem 0.9rem; - background: var(--agml-surface-strong); - font-size: 0.92rem; - color: var(--agml-text); -} - -.dropdownChevron { - margin-left: 0.5rem; -} - -.dropdownMenu { - position: absolute; - top: calc(100% + 0.4rem); - left: 0; - right: 0; - border-radius: 16px; - border: 1px solid var(--agml-border); - background: var(--agml-surface-strong); - padding: 0.4rem 0; - max-height: 240px; - overflow: auto; - box-shadow: var(--agml-shadow); - z-index: 40; -} - -.dropdownEmpty { - padding: 0.55rem 0.9rem; - font-size: 0.85rem; - color: var(--agml-muted); -} - -.dropdownOption { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.55rem 0.9rem; - width: 100%; - text-align: left; - border: none; - background: transparent; - cursor: pointer; - font-size: 0.9rem; -} - -.dropdownOptionSelected { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); -} - -.dropdownCheckbox { - width: 1.2rem; - height: 1.2rem; - border-radius: 6px; - border: 1px solid var(--agml-border-strong); - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 0.8rem; - color: var(--ifm-color-primary); -} - .clearButton { border: 1px solid var(--agml-border-strong); background: transparent; @@ -208,6 +125,54 @@ font-size: 0.85rem; } +.clickableRow { + cursor: pointer; +} + +.clickableRow:hover { + background: var(--agml-accent-soft); +} + +.modelName { + font-weight: 600; +} + +.expandChevron { + margin-left: 0.5rem; + color: var(--agml-muted); + font-size: 0.8rem; +} + +.detailsCell { + background: var(--agml-surface); + padding: 0.9rem 1.4rem 1.1rem; +} + +.detailsTable { + width: 100%; + border-collapse: collapse; +} + +.detailsTable th, +.detailsTable td { + text-align: left; + padding: 0.5rem 0.8rem; + font-size: 0.85rem; + color: var(--agml-text); + border-bottom: 1px solid var(--agml-border); +} + +.detailsTable th { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--agml-muted); +} + +.detailsTable tbody tr:last-child td { + border-bottom: none; +} + .pagination { display: flex; align-items: center; @@ -236,10 +201,6 @@ color: var(--agml-muted); } -html[data-theme='dark'] .dropdownOptionSelected { - color: var(--ifm-color-primary-lightest); -} - @media (max-width: 996px) { .hero { padding: 2rem 1.6rem; diff --git a/src/pages/leaderboard/index.tsx b/src/pages/leaderboard/index.tsx index b9b19de..e49c589 100644 --- a/src/pages/leaderboard/index.tsx +++ b/src/pages/leaderboard/index.tsx @@ -1,6 +1,7 @@ -import { useEffect, useMemo, useState } from 'react'; +import { Fragment, useEffect, useMemo, useState } from 'react'; import Layout from '@theme/Layout'; -import { computeGlobalLeaderboard, useGlobalPerformance } from '../../lib/performance'; +import { computeGlobalLeaderboard, formatGlobalResultTypeKey, globalResultTypeKey, useGlobalPerformance } from '../../lib/performance'; +import { MultiSelectDropdown } from '../../components/MultiSelectDropdown'; import styles from './index.module.css'; const MIN_APPEARANCES = 3; @@ -10,82 +11,31 @@ function toLabel(value: string) { return value.replace(/_/g, ' '); } -function MultiSelectDropdown({ - label, - options, - selected, - onToggle, -}: { - label: string; - options: string[]; - selected: string[]; - onToggle: (value: string) => void; -}) { - const [open, setOpen] = useState(false); - - return ( -
- - - {open && ( -
- {options.length === 0 &&
No options
} - {options.map((option) => { - const isSelected = selected.includes(option); - return ( - - ); - })} -
- )} -
- ); -} - export default function GlobalLeaderboardPage() { const { data: records, loading, error } = useGlobalPerformance(); const [cropTypes, setCropTypes] = useState([]); const [mlTasks, setMlTasks] = useState([]); + const [resultTypes, setResultTypes] = useState([]); + const [platforms, setPlatforms] = useState([]); - const { cropTypeOptions, mlTaskOptions } = useMemo(() => { + const { cropTypeOptions, mlTaskOptions, resultTypeOptions, platformOptions } = useMemo(() => { const cropSet = new Set(); const taskSet = new Set(); + const resultTypeSet = new Set(); + const platformSet = new Set(); for (const record of records) { record.crop_types?.forEach((crop) => cropSet.add(crop)); if (record.machine_learning_task) taskSet.add(record.machine_learning_task); + const resultTypeKey = globalResultTypeKey(record); + if (resultTypeKey) resultTypeSet.add(resultTypeKey); + if (record.platform) platformSet.add(record.platform); } return { cropTypeOptions: Array.from(cropSet).sort((a, b) => a.localeCompare(b)), mlTaskOptions: Array.from(taskSet).sort((a, b) => a.localeCompare(b)), + resultTypeOptions: Array.from(resultTypeSet).sort(), + platformOptions: Array.from(platformSet).sort((a, b) => a.localeCompare(b)), }; }, [records]); @@ -97,19 +47,29 @@ export default function GlobalLeaderboardPage() { setMlTasks((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); }; - const hasActiveFilters = cropTypes.length > 0 || mlTasks.length > 0; + const toggleResultType = (value: string) => { + setResultTypes((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + }; + + const togglePlatform = (value: string) => { + setPlatforms((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + }; + + const hasActiveFilters = cropTypes.length > 0 || mlTasks.length > 0 || resultTypes.length > 0 || platforms.length > 0; const clearFilters = () => { setCropTypes([]); setMlTasks([]); + setResultTypes([]); + setPlatforms([]); }; const leaderboard = useMemo( - () => computeGlobalLeaderboard(records, { cropTypes, mlTasks, minAppearances: MIN_APPEARANCES }), - [records, cropTypes, mlTasks] + () => computeGlobalLeaderboard(records, { cropTypes, mlTasks, resultTypes, platforms, minAppearances: MIN_APPEARANCES }), + [records, cropTypes, mlTasks, resultTypes, platforms] ); const [page, setPage] = useState(1); - useEffect(() => setPage(1), [cropTypes, mlTasks]); + useEffect(() => setPage(1), [cropTypes, mlTasks, resultTypes, platforms]); const pageCount = Math.max(1, Math.ceil(leaderboard.length / PAGE_SIZE)); const currentPage = Math.min(page, pageCount); @@ -118,6 +78,17 @@ export default function GlobalLeaderboardPage() { [leaderboard, currentPage] ); + const [expandedKeys, setExpandedKeys] = useState>(new Set()); + const toggleExpanded = (key: string) => { + setExpandedKeys((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + useEffect(() => setExpandedKeys(new Set()), [cropTypes, mlTasks, resultTypes, platforms, page]); + return (
@@ -134,8 +105,16 @@ export default function GlobalLeaderboardPage() {
- - + + + +
{hasActiveFilters && (
@@ -272,13 +276,36 @@ export function DatasetMetadataModal({ ))} - {(dataset.classes || dataset.stats_mean || dataset.stats_std) && ( + {(cropList.length > 0 || classList.length > 0 || dataset.stats_mean || dataset.stats_std) && (
- {dataset.classes && ( + {cropList.length > 0 && ( +
+

Crops

+
+ {(cropsExpanded ? cropList : cropList.slice(0, 8)).map((crop) => ( + {crop} + ))} + {!cropsExpanded && cropList.length > 8 && ( + + )} +
+
+ )} + {classList.length > 0 && (

Classes

-

{dataset.classes}

- +
+ {(classesExpanded ? classList : classList.slice(0, 8)).map((cls) => ( + {cls} + ))} + {!classesExpanded && classList.length > 8 && ( + + )} +
)} {(dataset.stats_mean || dataset.stats_std) && ( @@ -296,18 +323,34 @@ export function DatasetMetadataModal({ )}
- {hasExampleImage(dataset.examples_image_url) ? ( -
- {`Example -
- ) : ( -

No example image is available for this dataset.

- )} +
+

Sample image

+ {hasExampleImage(dataset.examples_image_url) ? ( +
+ {`Example +
+ ) : ( +

No example image is available for this dataset.

+ )} +

{loader.title}

{loader.body}

-
{loader.code}
+
+ {loader.code} + +
{(dataset.documentation || dataset.hf_link) && ( @@ -346,11 +389,18 @@ export function DatasetMetadataModal({ formatOption={(value) => METRIC_CATEGORY_LABELS[value as MetricCategory]} /> toggleFilter(setResultTypeFilter, value)} - formatOption={formatResultTypeKey} + label="Tuned" + options={tunedOptions} + selected={tunedFilter} + onToggle={(value) => toggleFilter(setTunedFilter, value)} + formatOption={formatTunedKey} + /> + toggleFilter(setOptimizedFilter, value)} + formatOption={formatOptimizedKey} /> toggleFilter(setPlatformFilter, value)} />
- +
- setTrainingTimeMin(event.target.value)} - className={styles.filterRangeInput} - /> -
- +
setTrainingSizeMin(event.target.value)} className={styles.filterRangeInput} /> - - setTrainingSizeMax(event.target.value)} - className={styles.filterRangeInput} - />
{hasActiveLeaderboardFilters && ( @@ -410,6 +444,7 @@ export function DatasetMetadataModal({ ) : ( + @@ -420,13 +455,14 @@ export function DatasetMetadataModal({ - - - - - - - + + + + + + + + @@ -435,38 +471,50 @@ export function DatasetMetadataModal({ const isExpanded = expandedRowKeys.has(rowKey); return ( - toggleExpandedRow(rowKey)} - aria-expanded={isExpanded} - > + + - - + + {isExpanded && ( - diff --git a/src/components/DatasetTable.module.css b/src/components/DatasetTable.module.css index 9629660..7dca769 100644 --- a/src/components/DatasetTable.module.css +++ b/src/components/DatasetTable.module.css @@ -51,8 +51,8 @@ .searchInput:focus { outline: none; - border-color: #2f8a5b; - box-shadow: 0 0 0 3px rgba(47, 138, 91, 0.15); + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--agml-accent-soft); } .tableWrap { @@ -79,7 +79,7 @@ appearance: none; border: none; background: transparent; - color: #2f8a5b; + color: var(--ifm-color-primary); padding: 0; font: inherit; cursor: pointer; @@ -92,7 +92,7 @@ } .datasetButton:focus-visible { - outline: 2px solid #2f8a5b; + outline: 2px solid var(--ifm-color-primary); outline-offset: 3px; border-radius: 4px; } diff --git a/src/components/LeaderboardDetailModal.module.css b/src/components/LeaderboardDetailModal.module.css new file mode 100644 index 0000000..77e3531 --- /dev/null +++ b/src/components/LeaderboardDetailModal.module.css @@ -0,0 +1,236 @@ +.backdrop { + position: fixed; + inset: 0; + z-index: 999; + display: grid; + place-items: center; + padding: 1.25rem; + background: var(--agml-modal-overlay); + backdrop-filter: blur(10px); +} + +.modal { + width: min(880px, 92vw); + max-height: min(88vh, 900px); + overflow-y: auto; + border-radius: 12px; + border: 1px solid var(--agml-border-strong); + background: var(--agml-surface-strong); + box-shadow: var(--agml-shadow-strong); + color: var(--agml-text); + padding: 1.75rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.1rem; +} + +.badgeRow { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + align-items: center; + margin-bottom: 0.5rem; +} + +.taskBadge { + font-family: var(--ifm-code-font-family); + font-size: 0.65rem; + padding: 0.15rem 0.5rem; + border-radius: 4px; + display: inline-block; + white-space: nowrap; +} + +.badgeClassification { + background: var(--agml-badge-classification-bg); + color: var(--agml-badge-classification-fg); +} + +.badgeDetection { + background: var(--agml-badge-detection-bg); + color: var(--agml-badge-detection-fg); +} + +.badgeSegmentation { + background: var(--agml-badge-segmentation-bg); + color: var(--agml-badge-segmentation-fg); +} + +.badgeOther { + background: var(--agml-badge-other-bg); + color: var(--agml-badge-other-fg); +} + +.resultTypeBadge { + font-family: var(--ifm-code-font-family); + font-size: 0.65rem; + padding: 0.15rem 0.5rem; + border-radius: 4px; + background: var(--agml-surface-soft); + color: var(--agml-muted); + display: inline-block; +} + +.title { + font-family: var(--ifm-code-font-family); + font-weight: 700; + font-size: 1.2rem; + margin: 0 0 0.4rem; +} + +.summaryLine { + font-family: var(--ifm-code-font-family); + font-size: 0.78rem; + color: var(--agml-muted); + margin: 0; +} + +.closeButton { + flex: 0 0 auto; + border: 1px solid var(--agml-border-strong); + background: var(--agml-surface-soft); + color: var(--agml-text); + width: 30px; + height: 30px; + border-radius: 6px; + cursor: pointer; + font-size: 15px; + line-height: 1; +} + +.closeButton:hover { + background: var(--agml-accent-soft); +} + +.sectionTitle { + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--agml-muted); + margin: 0 0 0.65rem; +} + +.tableWrap { + border: 1px solid var(--agml-border); + border-radius: 8px; + overflow: hidden; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; +} + +.table thead, +.table tbody tr { + background: transparent; +} + +.table thead tr { + border-bottom: none; +} + +.table tr:nth-child(2n) { + background-color: transparent; +} + +.table th, +.table td { + text-align: left; + padding: 0.55rem 0.75rem; + border: none; + border-bottom: 1px solid var(--agml-border); + vertical-align: top; +} + +.table th { + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--agml-muted); + background: var(--agml-surface-soft); +} + +.table tbody tr:last-child > td { + border-bottom: none; +} + +.clickableRow { + cursor: pointer; +} + +.clickableRow:hover { + background: var(--agml-accent-soft); +} + +.datasetCell { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.4rem; +} + +.datasetName { + display: flex; + align-items: center; + gap: 0.4rem; + background: none; + border: none; + padding: 0; + cursor: pointer; + font-family: var(--ifm-code-font-family); + color: var(--agml-text); + text-decoration: underline dashed var(--agml-muted); + text-decoration-thickness: 1px; +} + +.expandChevron { + color: var(--agml-muted); + font-size: 0.7rem; +} + +.viewDatasetLink { + font-family: var(--ifm-code-font-family); + font-size: 0.65rem; + padding: 0.15rem 0.5rem; + border-radius: 4px; + background: var(--agml-surface-soft); + color: var(--agml-muted); + text-decoration: none; + white-space: nowrap; +} + +.viewDatasetLink:hover { + background: var(--ifm-color-primary); + color: var(--agml-page-bg); + text-decoration: none; +} + +.scoresCell { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-family: var(--ifm-code-font-family); + color: var(--ifm-color-primary); + font-size: 0.75rem; +} + +.scoresCell span span { + color: var(--agml-muted); +} + +.notesRow td { + background: var(--agml-surface-soft); + border-top: 1px dashed var(--agml-border); + font-size: 0.78rem; + color: var(--agml-muted); +} diff --git a/src/components/LeaderboardDetailModal.tsx b/src/components/LeaderboardDetailModal.tsx new file mode 100644 index 0000000..05d1723 --- /dev/null +++ b/src/components/LeaderboardDetailModal.tsx @@ -0,0 +1,184 @@ +import { Fragment, useEffect, useState } from 'react'; +import Link from '@docusaurus/Link'; +import type { GlobalLeaderboardEntry } from '../lib/performance'; +import styles from './LeaderboardDetailModal.module.css'; + +function toLabel(value: string) { + return value.replace(/_/g, ' '); +} + +function formatPercentile(value: number | null) { + return value == null ? null : `${value.toFixed(0)}th pctl`; +} + +function formatScore(value: number) { + return value.toFixed(3); +} + +function formatResultType(entry: { variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean }) { + const base = entry.variant === 'fine-tuned' ? 'Fine-tuned' : entry.variant === 'zero-shot' ? 'Zero-shot' : '—'; + if (base === '—') return base; + return entry.optimized ? `${base} (optimized)` : base; +} + +function taskBadgeClass(task: string | null): string { + if (!task) return styles.badgeOther; + if (task.includes('classif')) return styles.badgeClassification; + if (task.includes('detect')) return styles.badgeDetection; + if (task.includes('segment')) return styles.badgeSegmentation; + return styles.badgeOther; +} + +export function LeaderboardDetailModal({ + entry, + open, + onClose, +}: { + entry: GlobalLeaderboardEntry | null; + open: boolean; + onClose: () => void; +}) { + useEffect(() => { + if (!open) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + + window.addEventListener('keydown', onKeyDown); + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener('keydown', onKeyDown); + }; + }, [open, onClose]); + + const [expandedKeys, setExpandedKeys] = useState>(new Set()); + useEffect(() => setExpandedKeys(new Set()), [entry]); + const toggleExpanded = (key: string) => { + setExpandedKeys((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + if (!open || entry == null) return null; + + return ( +
+
event.stopPropagation()} + > +
+
+
+ + {entry.machineLearningTask ? toLabel(entry.machineLearningTask) : 'Unknown task'} + + {entry.resultType} +
+

+ {entry.model} +

+

+ {entry.appearances} result{entry.appearances === 1 ? '' : 's'} across {entry.datasets.length} dataset + {entry.datasets.length === 1 ? '' : 's'} +

+
+ +
+ +

Datasets included ({entry.datasetDetails.length})

+
+
Model nameResult typeTrain / test / val images (%)Train / test configMetric scoreNormalized train / inf time (s/img)PlatformRankModelResultSplit %DataScoresNorm s/imgPlat.
#{entry.rank ?? index + 1} - + {formatResultType(entry)} {entry.splitBreakdown ?? '—'} {entry.datasetConfig ?? '—'}{formatMetricScore(entry)}{formatTrainInfTime(entry)} + {entry.metrics.length > 0 ? ( +
+ {entry.metrics.map((metric) => ( + {metric.label}: {metric.value.toFixed(3)} + ))} +
+ ) : ( + {formatMetricScore(entry)} + )} +
+
+ train {formatTimePerImage(entry.trainTimePerImage)} + inf {formatTimePerImage(entry.infTimePerImage)} +
+
{entry.platform ?? '—'}
+

Necessary notes

{entry.notes ?? 'No additional notes for this result.'}

+ + + + + + + + + + {entry.datasetDetails.map((detail, index) => { + const rowKey = `${detail.dataset}-${index}`; + const isExpanded = expandedKeys.has(rowKey); + const scoreLines = ( + [ + ['F1', 'f1'], + ['mAP', 'map'], + ['Precision', 'precision'], + ['Recall', 'recall'], + ] as [string, 'f1' | 'map' | 'precision' | 'recall'][] + ) + .map(([label, category]) => { + const score = detail.scores[category]; + const pctl = detail.percentiles[category]; + if (score == null || pctl == null) return null; + return [label, formatScore(score), formatPercentile(pctl)] as [string, string, string]; + }) + .filter((line): line is [string, string, string] => line != null); + + return ( + + toggleExpanded(rowKey)} aria-expanded={isExpanded}> + + + + + + {isExpanded && ( + + + + )} + + ); + })} + +
DatasetSplit %ConfigScores (percentile)
+
+ + event.stopPropagation()} + > + view dataset + +
+
{detail.splitBreakdown ?? '—'}{detail.datasetConfig ?? '—'} + {scoreLines.length === 0 ? ( + '—' + ) : ( +
+ {scoreLines.map(([label, score, pctl]) => ( + + {label} {score} ({pctl}) + + ))} +
+ )} +
+ {formatResultType(detail)} · {detail.platform ?? 'unknown platform'} +
+
+
+
+ ); +} diff --git a/src/components/MultiSelectDropdown.module.css b/src/components/MultiSelectDropdown.module.css index 9d0fc56..e0212dc 100644 --- a/src/components/MultiSelectDropdown.module.css +++ b/src/components/MultiSelectDropdown.module.css @@ -8,10 +8,14 @@ } .dropdownLabel { - font-size: 0.7rem; + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.14em; + letter-spacing: 0.06em; color: var(--agml-muted); + margin: 0 0 0.7rem; + display: block; } .dropdownTrigger { diff --git a/src/css/custom.css b/src/css/custom.css index c4ea3da..0619427 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Space+Grotesk:wght@400;500;600;700&family=Source+Code+Pro:wght@400;600&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap'); /** * Any CSS included here will be global. The classic template @@ -8,32 +8,43 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #2d8b5b; - --ifm-color-primary-dark: #267b50; - --ifm-color-primary-darker: #236f48; - --ifm-color-primary-darkest: #1b5a3a; - --ifm-color-primary-light: #3aa96d; - --ifm-color-primary-lighter: #45b879; - --ifm-color-primary-lightest: #66cf95; - --ifm-font-family-base: 'Space Grotesk', 'Noto Sans', sans-serif; - --ifm-heading-font-family: 'Fraunces', 'Ibarra Real Nova', serif; - --ifm-code-font-family: 'Source Code Pro', 'Fira Code', monospace; + --ifm-color-primary: oklch(0.5 0.14 150); + --ifm-color-primary-dark: oklch(0.45 0.14 150); + --ifm-color-primary-darker: oklch(0.42 0.14 150); + --ifm-color-primary-darkest: oklch(0.35 0.13 150); + --ifm-color-primary-light: oklch(0.55 0.14 150); + --ifm-color-primary-lighter: oklch(0.6 0.14 150); + --ifm-color-primary-lightest: oklch(0.7 0.13 150); + --ifm-font-family-base: 'Inter', 'Noto Sans', sans-serif; + --ifm-heading-font-family: 'Inter', 'Noto Sans', sans-serif; + --ifm-code-font-family: 'IBM Plex Mono', 'Fira Code', monospace; --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(16, 28, 22, 0.08); - --agml-page-bg: linear-gradient(130deg, #f6f3ec 0%, #eaf4ee 45%, #f4efe6 100%); - --agml-surface: rgba(255, 255, 255, 0.88); - --agml-surface-strong: rgba(255, 255, 255, 0.95); - --agml-surface-soft: rgba(234, 244, 238, 0.8); - --agml-border: rgba(25, 45, 33, 0.12); - --agml-border-strong: rgba(25, 45, 33, 0.18); - --agml-shadow: 0 20px 40px rgba(25, 45, 33, 0.08); - --agml-shadow-strong: 0 28px 60px rgba(25, 45, 33, 0.12); - --agml-text: #1c2b21; - --agml-muted: #4a5f52; - --agml-accent-soft: rgba(45, 139, 91, 0.15); - --agml-accent-border: rgba(45, 139, 91, 0.35); + --docusaurus-highlighted-code-line-bg: oklch(0.16 0.01 145 / 0.08); + --agml-page-bg: oklch(0.985 0.006 145); + --agml-surface: oklch(0.97 0.01 145); + --agml-surface-strong: oklch(1 0.002 145); + --agml-surface-soft: oklch(0.965 0.008 145); + --agml-border: oklch(0.85 0.012 145); + --agml-border-strong: oklch(0.78 0.014 145); + --agml-shadow: 0 16px 32px oklch(0 0 0 / 0.06); + --agml-shadow-strong: 0 24px 48px oklch(0 0 0 / 0.09); + --agml-text: oklch(0.2 0.012 145); + --agml-muted: oklch(0.45 0.012 145); + --agml-accent-soft: oklch(0.92 0.06 150); + --agml-accent-border: color-mix(in oklch, var(--ifm-color-primary) 35%, var(--agml-border)); --agml-warning-soft: rgba(189, 139, 76, 0.12); --agml-warning-text: #7a4e2a; + --agml-modal-overlay: oklch(0 0 0 / 0.3); + --agml-badge-classification-bg: oklch(0.89 0.10 145); + --agml-badge-classification-fg: oklch(0.34 0.13 145); + --agml-badge-detection-bg: oklch(0.89 0.08 175); + --agml-badge-detection-fg: oklch(0.36 0.12 175); + --agml-badge-segmentation-bg: oklch(0.89 0.07 110); + --agml-badge-segmentation-fg: oklch(0.36 0.11 110); + --agml-badge-other-bg: oklch(0.91 0.01 145); + --agml-badge-other-fg: oklch(0.4 0.01 145); + --agml-tag-bg: oklch(0.91 0.02 145); + --agml-tag-fg: oklch(0.36 0.03 145); --ifm-background-color: transparent; --ifm-navbar-background-color: var(--agml-surface); --ifm-footer-background-color: var(--agml-surface); @@ -44,28 +55,39 @@ /* For readability concerns, you should choose a lighter palette in dark mode. */ html[data-theme='dark'] { - --ifm-color-primary: #5ccf9a; - --ifm-color-primary-dark: #44b983; - --ifm-color-primary-darker: #3aa975; - --ifm-color-primary-darkest: #2f8a5f; - --ifm-color-primary-light: #6fdaaa; - --ifm-color-primary-lighter: #7fe1b6; - --ifm-color-primary-lightest: #9fe9c9; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.35); - --agml-page-bg: linear-gradient(130deg, #0d1410 0%, #101b15 45%, #151f18 100%); - --agml-surface: rgba(18, 26, 21, 0.88); - --agml-surface-strong: rgba(21, 30, 24, 0.96); - --agml-surface-soft: rgba(32, 43, 36, 0.9); - --agml-border: rgba(255, 255, 255, 0.09); - --agml-border-strong: rgba(255, 255, 255, 0.14); - --agml-shadow: 0 20px 40px rgba(0, 0, 0, 0.28); - --agml-shadow-strong: 0 28px 60px rgba(0, 0, 0, 0.34); - --agml-text: #e8f0ea; - --agml-muted: #a6b7ad; - --agml-accent-soft: rgba(92, 207, 154, 0.16); - --agml-accent-border: rgba(92, 207, 154, 0.3); + --ifm-color-primary: oklch(0.72 0.15 150); + --ifm-color-primary-dark: oklch(0.65 0.15 150); + --ifm-color-primary-darker: oklch(0.6 0.15 150); + --ifm-color-primary-darkest: oklch(0.5 0.14 150); + --ifm-color-primary-light: oklch(0.78 0.14 150); + --ifm-color-primary-lighter: oklch(0.83 0.13 150); + --ifm-color-primary-lightest: oklch(0.88 0.11 150); + --docusaurus-highlighted-code-line-bg: oklch(0 0 0 / 0.35); + --agml-page-bg: oklch(0.16 0.01 145); + --agml-surface: oklch(0.18 0.012 145); + --agml-surface-strong: oklch(0.19 0.012 145); + --agml-surface-soft: oklch(0.17 0.01 145); + --agml-border: oklch(0.28 0.014 145); + --agml-border-strong: oklch(0.3 0.014 145); + --agml-shadow: 0 16px 32px oklch(0 0 0 / 0.28); + --agml-shadow-strong: 0 24px 48px oklch(0 0 0 / 0.34); + --agml-text: oklch(0.92 0.006 145); + --agml-muted: oklch(0.65 0.01 145); + --agml-accent-soft: oklch(0.22 0.05 150); + --agml-accent-border: color-mix(in oklch, var(--ifm-color-primary) 30%, var(--agml-border)); --agml-warning-soft: rgba(189, 139, 76, 0.18); --agml-warning-text: #d9b084; + --agml-modal-overlay: oklch(0 0 0 / 0.62); + --agml-badge-classification-bg: oklch(0.30 0.10 145); + --agml-badge-classification-fg: oklch(0.88 0.15 145); + --agml-badge-detection-bg: oklch(0.30 0.09 175); + --agml-badge-detection-fg: oklch(0.86 0.13 175); + --agml-badge-segmentation-bg: oklch(0.30 0.07 110); + --agml-badge-segmentation-fg: oklch(0.86 0.12 110); + --agml-badge-other-bg: oklch(0.30 0.02 145); + --agml-badge-other-fg: oklch(0.8 0.02 145); + --agml-tag-bg: oklch(0.24 0.014 145); + --agml-tag-fg: oklch(0.75 0.012 145); --ifm-background-color: transparent; --ifm-navbar-background-color: var(--agml-surface); --ifm-footer-background-color: var(--agml-surface); diff --git a/src/lib/datasets.ts b/src/lib/datasets.ts index 83c174d..f319580 100644 --- a/src/lib/datasets.ts +++ b/src/lib/datasets.ts @@ -340,3 +340,27 @@ export function formatDisplayLocation(value: string | string[] | null | undefine if (value == null) return '—'; return Array.isArray(value) ? value.join(', ') : value; } + +export interface DatasetStats { + datasetCount: number; + imageCount: number; + taskTypeCount: number; +} + +// Shared by the homepage stats row and the dataset search page's hero stats, so both +// report the same real numbers instead of two slightly different ad-hoc counts. +export function computeDatasetStats(datasets: Dataset[]): DatasetStats { + const topLevel = datasets.filter((dataset) => !dataset.parent_dataset); + const imageCount = topLevel + .filter((dataset) => !dataset.name.startsWith('iNatAg-mini')) + .reduce((sum, dataset) => sum + (dataset.num_images ?? 0), 0); + const taskTypeCount = new Set( + datasets.map((dataset) => dataset.machine_learning_task).filter((task): task is string => Boolean(task)) + ).size; + + return { + datasetCount: topLevel.length, + imageCount, + taskTypeCount, + }; +} diff --git a/src/lib/performance.ts b/src/lib/performance.ts index 83f0796..de4bbe4 100644 --- a/src/lib/performance.ts +++ b/src/lib/performance.ts @@ -19,6 +19,7 @@ export interface PerformanceEntry { platform: string | null; metrics: MetricValue[]; metricCategories: MetricCategory[]; + categoryScores: CategoryScores; } export type MetricCategory = 'f1' | 'map' | 'precision_recall' | 'other'; @@ -90,6 +91,7 @@ function normalizeEntry(raw: unknown): PerformanceEntry | null { platform: toText(raw.platform ?? raw.device), metrics: [], metricCategories: [], + categoryScores: { f1: null, map: null, precision: null, recall: null }, }; } @@ -136,6 +138,31 @@ function isMapMetricKey(key: string): boolean { return /^m?ap([_@-]|$)/i.test(key); } +export interface CategoryScores { + f1: number | null; + map: number | null; + precision: number | null; + recall: number | null; +} + +// Every run can report several metric families at once (F1, mAP, precision, recall), +// independent of the single metric resolveMetricKey picks for the legacy per-dataset rank. +// These four category scores back the leaderboard's four sortable global columns — precision +// and recall are ranked/percentiled independently, not averaged into one blended score. +// Mirrored in scripts/generate-datasets.mjs — keep in sync. +function computeCategoryScores(metrics: Record): CategoryScores { + const f1 = isFiniteNumber(metrics.f1) ? metrics.f1 : null; + let map: number | null = null; + for (const [key, value] of Object.entries(metrics)) { + if (isMapMetricKey(key) && isFiniteNumber(value)) { + map = map == null ? value : Math.max(map, value); + } + } + const precision = isFiniteNumber(metrics.precision) ? metrics.precision : null; + const recall = isFiniteNumber(metrics.recall) ? metrics.recall : null; + return { f1, map, precision, recall }; +} + function formatMapMetricLabel(key: string): string { const normalized = key.toLowerCase(); if (normalized === 'map' || normalized === 'map50' || normalized === 'map_50' || normalized === 'mAP@0.5'.toLowerCase()) { @@ -239,6 +266,7 @@ function makeLeaderboardRow( platform: toText(entry.device), metrics: metricValues, metricCategories: Array.from(new Set(metricValues.map((metric) => classifyMetricLabel(metric.label)))), + categoryScores: computeCategoryScores(metrics), }; } @@ -303,23 +331,36 @@ function normalizePerformance(json: unknown): DatasetPerformance { return { metric: null, entries: [] }; } +export interface CategoryPercentiles { + f1: number | null; + map: number | null; + precision: number | null; + recall: number | null; +} + export interface GlobalPerformanceRecord { model: string; dataset: string; - percentile: number; + percentiles: CategoryPercentiles; + scores: CategoryScores; crop_types: string[] | null; machine_learning_task: string | null; variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean; platform: string | null; + splitBreakdown: string | null; + datasetConfig: string | null; } export interface GlobalLeaderboardDatasetDetail { dataset: string; - percentile: number; + percentiles: CategoryPercentiles; + scores: CategoryScores; variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean; platform: string | null; + splitBreakdown: string | null; + datasetConfig: string | null; } export function globalResultTypeKey(record: { variant: 'zero-shot' | 'fine-tuned' | null; optimized: boolean }): string | null { @@ -337,7 +378,10 @@ export function formatGlobalResultTypeKey(key: string) { export interface GlobalLeaderboardEntry { model: string; machineLearningTask: string | null; - averagePercentile: number; + avgF1Percentile: number | null; + avgMapPercentile: number | null; + avgPrecisionPercentile: number | null; + avgRecallPercentile: number | null; appearances: number; datasets: string[]; fineTunedDatasets: string[]; @@ -346,12 +390,33 @@ export interface GlobalLeaderboardEntry { datasetDetails: GlobalLeaderboardDatasetDetail[]; } +function normalizeCategoryPercentiles(raw: unknown): CategoryPercentiles { + const record = isRecord(raw) ? raw : {}; + return { + f1: toNumber(record.f1), + map: toNumber(record.map), + precision: toNumber(record.precision), + recall: toNumber(record.recall), + }; +} + +function normalizeCategoryScores(raw: unknown): CategoryScores { + const record = isRecord(raw) ? raw : {}; + return { + f1: toNumber(record.f1), + map: toNumber(record.map), + precision: toNumber(record.precision), + recall: toNumber(record.recall), + }; +} + function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord | null { if (!isRecord(raw)) return null; const model = toText(raw.model); const dataset = toText(raw.dataset); - const percentile = toNumber(raw.percentile); - if (!model || !dataset || percentile == null) return null; + const percentiles = normalizeCategoryPercentiles(raw.percentiles); + if (!model || !dataset) return null; + if (percentiles.f1 == null && percentiles.map == null && percentiles.precision == null && percentiles.recall == null) return null; const cropTypes = Array.isArray(raw.crop_types) ? raw.crop_types.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0) @@ -362,12 +427,15 @@ function normalizeGlobalPerformanceRecord(raw: unknown): GlobalPerformanceRecord return { model, dataset, - percentile, + percentiles, + scores: normalizeCategoryScores(raw.scores), crop_types: cropTypes?.length ? cropTypes : null, machine_learning_task: toText(raw.machine_learning_task), variant, optimized: Boolean(raw.optimized), platform: toText(raw.platform), + splitBreakdown: toText(raw.splitBreakdown), + datasetConfig: toText(raw.datasetConfig), }; } @@ -387,17 +455,20 @@ export function computeGlobalLeaderboard( cropTypes?: string[]; mlTasks?: string[]; resultTypes?: string[]; + tuned?: ('tuned' | 'not-tuned')[]; + optimizedValues?: ('optimized' | 'not-optimized')[]; platforms?: string[]; minAppearances?: number; } = {} ): GlobalLeaderboardEntry[] { - const { cropTypes = [], mlTasks = [], resultTypes = [], platforms = [], minAppearances = 3 } = options; + const { cropTypes = [], mlTasks = [], resultTypes = [], tuned = [], optimizedValues = [], platforms = [], minAppearances = 3 } = options; const stats = new Map< string, { model: string; machineLearningTask: string | null; - totalPercentile: number; + categoryTotals: Record; + categoryCounts: Record; appearances: number; datasets: Set; fineTunedDatasets: Set; @@ -414,6 +485,14 @@ export function computeGlobalLeaderboard( const resultTypeKey = globalResultTypeKey(record); if (!resultTypeKey || !resultTypes.includes(resultTypeKey)) continue; } + if (tuned.length) { + const tunedKey = record.variant === 'fine-tuned' ? 'tuned' : 'not-tuned'; + if (!tuned.includes(tunedKey)) continue; + } + if (optimizedValues.length) { + const optimizedKey = record.optimized ? 'optimized' : 'not-optimized'; + if (!optimizedValues.includes(optimizedKey)) continue; + } if (platforms.length && !(record.platform && platforms.includes(record.platform))) continue; const key = `${record.model}|||${record.machine_learning_task ?? ''}`; @@ -422,7 +501,8 @@ export function computeGlobalLeaderboard( { model: record.model, machineLearningTask: record.machine_learning_task, - totalPercentile: 0, + categoryTotals: { f1: 0, map: 0, precision: 0, recall: 0 }, + categoryCounts: { f1: 0, map: 0, precision: 0, recall: 0 }, appearances: 0, datasets: new Set(), fineTunedDatasets: new Set(), @@ -430,7 +510,12 @@ export function computeGlobalLeaderboard( optimized: false, datasetDetails: [] as GlobalLeaderboardDatasetDetail[], }; - entryStats.totalPercentile += record.percentile; + (Object.keys(record.percentiles) as (keyof CategoryPercentiles)[]).forEach((categoryKey) => { + const value = record.percentiles[categoryKey]; + if (value == null) return; + entryStats.categoryTotals[categoryKey] += value; + entryStats.categoryCounts[categoryKey] += 1; + }); entryStats.appearances += 1; entryStats.datasets.add(record.dataset); if (record.variant === 'fine-tuned') entryStats.fineTunedDatasets.add(record.dataset); @@ -438,20 +523,28 @@ export function computeGlobalLeaderboard( if (record.optimized) entryStats.optimized = true; entryStats.datasetDetails.push({ dataset: record.dataset, - percentile: record.percentile, + percentiles: record.percentiles, + scores: record.scores, variant: record.variant, optimized: record.optimized, platform: record.platform, + splitBreakdown: record.splitBreakdown, + datasetConfig: record.datasetConfig, }); stats.set(key, entryStats); } + const average = (total: number, count: number) => (count > 0 ? total / count : null); + return Array.from(stats.values()) .filter((entryStats) => entryStats.appearances >= minAppearances) .map((entryStats) => ({ model: entryStats.model, machineLearningTask: entryStats.machineLearningTask, - averagePercentile: entryStats.totalPercentile / entryStats.appearances, + avgF1Percentile: average(entryStats.categoryTotals.f1, entryStats.categoryCounts.f1), + avgMapPercentile: average(entryStats.categoryTotals.map, entryStats.categoryCounts.map), + avgPrecisionPercentile: average(entryStats.categoryTotals.precision, entryStats.categoryCounts.precision), + avgRecallPercentile: average(entryStats.categoryTotals.recall, entryStats.categoryCounts.recall), appearances: entryStats.appearances, datasets: Array.from(entryStats.datasets).sort(), fineTunedDatasets: Array.from(entryStats.fineTunedDatasets).sort(), @@ -459,7 +552,7 @@ export function computeGlobalLeaderboard( optimized: entryStats.optimized, datasetDetails: entryStats.datasetDetails.sort((a, b) => a.dataset.localeCompare(b.dataset)), })) - .sort((a, b) => b.averagePercentile - a.averagePercentile); + .sort((a, b) => (b.avgMapPercentile ?? b.avgF1Percentile ?? 0) - (a.avgMapPercentile ?? a.avgF1Percentile ?? 0)); } export function useGlobalPerformance(): { diff --git a/src/pages/datasets/index.module.css b/src/pages/datasets/index.module.css index dc0149c..50c3ab2 100644 --- a/src/pages/datasets/index.module.css +++ b/src/pages/datasets/index.module.css @@ -1,390 +1,139 @@ .page { - position: relative; - overflow: visible; - padding: 3.5rem 1.5rem 4rem; background: var(--agml-page-bg); color: var(--agml-text); -} - -:global(html), -:global(body) { - overflow-y: auto; - overflow-x: hidden; -} - -:global(#__docusaurus) { - min-height: 100%; -} - -.page::before, -.page::after { - content: ''; - position: absolute; - border-radius: 999px; - filter: blur(0); - opacity: 0.45; - pointer-events: none; -} - -.page::before { - width: 420px; - height: 420px; - top: -120px; - right: -120px; - background: radial-gradient(circle at top, rgba(64, 131, 91, 0.55), transparent 70%); -} - -.page::after { - width: 360px; - height: 360px; - bottom: -140px; - left: -120px; - background: radial-gradient(circle at bottom, var(--agml-warning-soft), transparent 70%); -} - -.hero { - position: relative; - z-index: 1; - max-width: 1600px; - margin: 0 auto 2.5rem; - padding: 2.5rem 2.5rem 2rem; - border-radius: 28px; - border: 1px solid var(--agml-border); - background: var(--agml-surface); - box-shadow: var(--agml-shadow-strong); -} - -.heroContent { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.heroTag { - text-transform: uppercase; - letter-spacing: 0.2em; - font-size: 0.75rem; - font-weight: 600; - color: var(--agml-muted); - margin: 0; -} - -.heroTitle { - font-size: clamp(2.2rem, 2.8vw, 3.4rem); - margin: 0; - color: var(--agml-text); -} - -.heroSubtitle { - margin: 0; - font-size: 1.05rem; - color: var(--agml-muted); - max-width: 720px; -} - -.heroStats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.heroStat { + min-height: calc(100vh - 60px); display: flex; flex-direction: column; - gap: 0.35rem; - padding: 0.9rem 1.1rem; - border-radius: 18px; - background: var(--agml-surface-soft); - border: 1px solid rgba(25, 45, 33, 0.08); - font-weight: 600; - color: var(--agml-text); } -.heroStat span:last-child { - font-size: 0.78rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--agml-muted); - font-weight: 500; -} - -.controls { - position: relative; - z-index: 30; - max-width: 1600px; - margin: 0 auto 1.5rem; +.toolbar { display: flex; - flex-wrap: wrap; - gap: 1.2rem; - align-items: flex-end; - justify-content: space-between; - padding: 1.5rem 2rem; - border-radius: 22px; + align-items: center; + gap: 1rem; + padding: 0.9rem 1.5rem; + border-bottom: 1px solid var(--agml-border); background: var(--agml-surface); - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); -} - -.searchBlock { - flex: 1 1 260px; - min-width: 240px; -} - -.searchLabel { - display: block; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--agml-muted); - margin-bottom: 0.4rem; } .searchInput { - width: 100%; - border-radius: 999px; + flex: 1; + max-width: 480px; + border-radius: 6px; border: 1px solid var(--agml-border-strong); background: var(--agml-surface-strong); - padding: 0.7rem 1rem; - font-size: 0.95rem; + padding: 0.55rem 0.8rem; + font-family: var(--ifm-code-font-family); + font-size: 0.85rem; color: var(--agml-text); - box-shadow: inset 0 0 0 1px rgba(25, 45, 33, 0.04); } .searchInput:focus { outline: none; - border-color: #2d8b5b; - box-shadow: 0 0 0 4px rgba(45, 139, 91, 0.15); + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--agml-accent-soft); } -.dropdownRow { +.toolbarRight { + margin-left: auto; display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -.toggle { - display: inline-flex; align-items: center; - gap: 0.5rem; - font-size: 0.85rem; - color: #4a5f52; - background: rgba(255, 255, 255, 0.7); - border: 1px solid rgba(25, 45, 33, 0.12); - padding: 0.5rem 0.8rem; - border-radius: 999px; -} - -.toggle input { - accent-color: #2d8b5b; -} - -.dropdown { - position: relative; - display: flex; - flex-direction: column; - gap: 0.4rem; - min-width: 160px; - z-index: 31; + gap: 1rem; } -.dropdownLabel { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.14em; +.resultCount { + font-family: var(--ifm-code-font-family); + font-size: 0.78rem; color: var(--agml-muted); + white-space: nowrap; } -.dropdownTrigger { - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 14px; +.clearButton { border: 1px solid var(--agml-border-strong); - padding: 0.6rem 0.9rem; - background: var(--agml-surface-strong); - font-size: 0.92rem; + background: transparent; + border-radius: 999px; + padding: 0.4rem 0.9rem; + font-size: 0.8rem; color: var(--agml-text); + cursor: pointer; + white-space: nowrap; } -.dropdownChevron { - margin-left: 0.5rem; -} - -.dropdownMenu { - position: absolute; - top: calc(100% + 0.4rem); - left: 0; - right: 0; - border-radius: 16px; - border: 1px solid var(--agml-border); - background: var(--agml-surface-strong); - padding: 0.4rem 0; - max-height: 240px; - overflow: auto; - box-shadow: var(--agml-shadow); - z-index: 40; +.body { + flex: 1; + display: flex; + min-height: 0; } -.dropdownOption { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.55rem 0.9rem; - width: 100%; - text-align: left; - border: none; - background: transparent; - cursor: pointer; - font-size: 0.9rem; +.sidebar { + width: 260px; + flex-shrink: 0; + border-right: 1px solid var(--agml-border); + background: var(--agml-surface-soft); + padding: 1.25rem 1.1rem; + overflow-y: auto; } -.dropdownOptionSelected { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); +.filterGroup { + margin-bottom: 1.6rem; } -.dropdownCheckbox { - width: 1.2rem; - height: 1.2rem; - border-radius: 6px; - border: 1px solid var(--agml-border-strong); - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 0.8rem; - color: var(--ifm-color-primary); +.filterGroupLabel { + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--agml-muted); + margin: 0 0 0.7rem; } -.chipsSection { - position: relative; - z-index: 1; - max-width: 1600px; - margin: 0 auto 1.5rem; +.filterGroupOptions { display: flex; flex-direction: column; - gap: 1rem; + gap: 0.55rem; } -.chipGroup { +.checkboxRow { display: flex; - flex-wrap: wrap; - gap: 0.6rem; align-items: center; -} - -.chipLabel { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--agml-muted); - margin-right: 0.4rem; -} - -.chipBase { - border-radius: 999px; - padding: 0.45rem 0.85rem; - font-size: 0.78rem; - border: 1px solid transparent; - transition: all 0.2s ease; -} - -.tag { - border-radius: 999px; - padding: 0.35rem 0.7rem; - font-size: 0.75rem; - border: 1px solid transparent; -} - -.chip { + justify-content: space-between; + gap: 0.5rem; + font-size: 0.82rem; cursor: pointer; -} - -.chipInactive { - background: var(--agml-surface-strong); - border-color: var(--agml-border); - color: var(--agml-text); -} - -.chipActive { - background: var(--ifm-color-primary); - color: #fff; - border-color: var(--ifm-color-primary); -} - -.chipAccent { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); - border-color: var(--agml-accent-border); -} - -.chipMuted { - background: rgba(127, 143, 134, 0.12); color: var(--agml-text); } -.chipOutline { - background: rgba(127, 143, 134, 0.08); - color: var(--agml-muted); -} - -.chipCount { - background: var(--agml-warning-soft); - color: var(--agml-warning-text); -} - -.activeFilters { - position: relative; - z-index: 1; - max-width: 1600px; - margin: 0 auto 1.5rem; +.checkboxRowLeft { display: flex; - flex-wrap: wrap; align-items: center; - gap: 0.8rem; - justify-content: space-between; -} - -.activeList { - display: flex; - flex-wrap: wrap; - gap: 0.6rem; + gap: 0.5rem; } -.activeChip { - border: none; - background: var(--agml-text); - color: #fff; - border-radius: 999px; - padding: 0.4rem 0.85rem; - font-size: 0.75rem; +.checkbox { + width: 15px; + height: 15px; + accent-color: var(--ifm-color-primary); cursor: pointer; } -.clearButton { - border: 1px solid var(--agml-border-strong); - background: transparent; - border-radius: 999px; - padding: 0.45rem 0.9rem; - font-size: 0.8rem; - color: var(--agml-text); - cursor: pointer; +.checkboxCount { + font-family: var(--ifm-code-font-family); + font-size: 0.7rem; + color: var(--agml-muted); } .results { - position: relative; - z-index: 1; - max-width: 1600px; - margin: 0 auto; + flex: 1; + overflow-y: auto; + padding: 1.5rem; } .cardGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.2rem; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 1rem; } - .cardButton { appearance: none; border: none; @@ -409,77 +158,100 @@ .card { background: var(--agml-surface-strong); - border-radius: 20px; - padding: 1.2rem 1.3rem 1.4rem; + border-radius: 8px; + padding: 1rem; border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + display: flex; + flex-direction: column; + gap: 0.75rem; + transition: border-color 0.15s ease; } .cardButton:hover .card, .cardButton:focus-visible .card { - transform: translateY(-4px); - box-shadow: 0 28px 50px rgba(25, 45, 33, 0.16); - border-color: rgba(45, 139, 91, 0.4); + border-color: var(--agml-accent-border); } .cardButton:focus-visible .card { outline: 2px solid var(--ifm-color-primary); - outline-offset: 3px; + outline-offset: 2px; } .cardTitle { margin: 0; - font-size: 1.05rem; + font-family: var(--ifm-code-font-family); + font-weight: 600; + font-size: 0.9rem; color: var(--agml-text); line-height: 1.3; } -.cardDetails { - display: grid; - gap: 0.55rem; - margin-top: 0.75rem; -} - -.cardDetail { +.cardTags { display: flex; - flex-direction: column; - gap: 0.15rem; + flex-wrap: wrap; + gap: 0.4rem; } -.cardDetailLabel { +.taskBadge { + font-family: var(--ifm-code-font-family); font-size: 0.68rem; - text-transform: uppercase; - letter-spacing: 0.16em; - color: var(--agml-muted); + padding: 0.15rem 0.5rem; + border-radius: 4px; + display: inline-block; + white-space: nowrap; } -.cardDetailValue { - font-size: 0.92rem; - color: var(--agml-text); - font-weight: 600; +.badgeClassification { + background: var(--agml-badge-classification-bg); + color: var(--agml-badge-classification-fg); +} + +.badgeDetection { + background: var(--agml-badge-detection-bg); + color: var(--agml-badge-detection-fg); +} + +.badgeSegmentation { + background: var(--agml-badge-segmentation-bg); + color: var(--agml-badge-segmentation-fg); +} + +.badgeOther { + background: var(--agml-badge-other-bg); + color: var(--agml-badge-other-fg); } -.codeBadge { +.tag { + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + padding: 0.15rem 0.5rem; + border-radius: 4px; display: inline-block; - padding: 0.25rem 0.6rem; - border-radius: 12px; - background: rgba(25, 45, 33, 0.08); - font-family: 'Source Code Pro', monospace; - font-size: 0.85rem; + white-space: nowrap; + background: var(--agml-tag-bg); + color: var(--agml-tag-fg); } -.cardTags { +.cardFooter { display: flex; - flex-wrap: wrap; - gap: 0.45rem; - margin-top: 0.7rem; + flex-direction: column; + gap: 0.3rem; + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; + color: var(--agml-muted); + padding-top: 0.6rem; + border-top: 1px solid var(--agml-border); } -.cardMeta { - margin: 0.7rem 0 0; - color: #4a5f52; - font-size: 0.85rem; +.cardFooterRow { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.5rem; +} + +.cardFooterAugmented { + color: var(--ifm-color-primary); } .status { @@ -489,17 +261,16 @@ .loadMore { display: flex; justify-content: center; - margin-top: 2rem; + margin-top: 1.75rem; } .loadMoreButton { border-radius: 999px; - padding: 0.7rem 1.4rem; + padding: 0.6rem 1.3rem; border: 1px solid var(--ifm-color-primary); background: var(--ifm-color-primary); - color: #fff; + color: var(--agml-page-bg); font-weight: 600; - box-shadow: var(--agml-shadow); cursor: pointer; } @@ -508,49 +279,14 @@ border-color: var(--ifm-color-primary-dark); } -.loadMoreButton:focus-visible { - outline: none; - box-shadow: 0 0 0 4px rgba(45, 139, 91, 0.25), var(--agml-shadow); -} - -html[data-theme='dark'] .page::before { - opacity: 0.25; -} - -html[data-theme='dark'] .page::after { - opacity: 0.2; -} - -html[data-theme='dark'] .dropdownOptionSelected { - color: var(--ifm-color-primary-lightest); -} - -html[data-theme='dark'] .chipActive { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); - border-color: var(--agml-accent-border); -} - -html[data-theme='dark'] .activeChip { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); - border: 1px solid var(--agml-accent-border); -} - -html[data-theme='dark'] .toggle input { - accent-color: var(--ifm-color-primary-light); -} - @media (max-width: 996px) { - .hero { - padding: 2rem 1.6rem; - } - - .controls { - padding: 1.2rem 1.5rem; + .body { + flex-direction: column; } - .heroStats { - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + .sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--agml-border); } } diff --git a/src/pages/datasets/index.tsx b/src/pages/datasets/index.tsx index b866d3a..2e7e776 100644 --- a/src/pages/datasets/index.tsx +++ b/src/pages/datasets/index.tsx @@ -2,12 +2,11 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } f import Layout from '@theme/Layout'; import { useHistory, useLocation } from '@docusaurus/router'; import { DatasetMetadataModal } from '../../components/DatasetMetadataModal'; +import { MultiSelectDropdown } from '../../components/MultiSelectDropdown'; import styles from './index.module.css'; -import { filterDatasets, formatDisplayLocation, useDatasets } from '../../lib/datasets'; +import { computeDatasetStats, filterDatasets, formatDisplayLocation, useDatasets } from '../../lib/datasets'; -const CHIP_CLASSES = `${styles.chip} ${styles.chipBase}`; - -type FilterKind = 'dropdown' | 'chips'; +type FilterKind = 'checkbox' | 'dropdown'; type DatasetFilterConfig = { key: string; @@ -15,46 +14,27 @@ type DatasetFilterConfig = { field: keyof import('../../lib/datasets').Dataset; kind: FilterKind; formatOption?: (value: string) => string; - chipLabel?: string; mode?: 'exact' | 'containsAny'; }; const DATASET_FILTERS: DatasetFilterConfig[] = [ { key: 'ml_task', - label: 'Task', - chipLabel: 'Task', + label: 'Task Type', field: 'machine_learning_task', - kind: 'chips', + kind: 'checkbox', formatOption: (value) => value.replace(/_/g, ' '), }, { key: 'ag_task', - label: 'Ag task', + label: 'Agricultural Task', field: 'agricultural_task', kind: 'dropdown', formatOption: (value) => value.replace(/_/g, ' '), }, - { - key: 'environment', - label: 'Environment', - chipLabel: 'Environment', - field: 'environment', - kind: 'chips', - formatOption: (value) => value.charAt(0).toUpperCase() + value.slice(1), - }, - { - key: 'augmented_counterpart', - label: 'Augmented counterpart', - chipLabel: 'Augmented', - field: 'augmented_counterpart', - kind: 'chips', - formatOption: (value) => (value === 'yes' ? 'Yes' : 'No'), - }, { key: 'crop_types', - label: 'Crop type', - chipLabel: 'Crop type', + label: 'Crop', field: 'crop_types', kind: 'dropdown', mode: 'containsAny', @@ -63,12 +43,18 @@ const DATASET_FILTERS: DatasetFilterConfig[] = [ { key: 'location', label: 'Location', - chipLabel: 'Location', field: 'location', kind: 'dropdown', mode: 'containsAny', formatOption: (value) => value, }, + { + key: 'environment', + label: 'Environment', + field: 'environment', + kind: 'checkbox', + formatOption: (value) => value.charAt(0).toUpperCase() + value.slice(1), + }, { key: 'platform', label: 'Platform', @@ -78,22 +64,22 @@ const DATASET_FILTERS: DatasetFilterConfig[] = [ }, { key: 'real', - label: 'Data', - chipLabel: 'Data', + label: 'Data Type', field: 'real_or_synthetic', - kind: 'chips', + kind: 'checkbox', formatOption: (value) => value, }, + { + key: 'augmented_counterpart', + label: 'Augmented', + field: 'augmented_counterpart', + kind: 'checkbox', + formatOption: (value) => (value === 'yes' ? 'Yes' : 'No'), + }, ]; type FilterKey = (typeof DATASET_FILTERS)[number]['key']; -type ActiveFilterChip = { - key: FilterKey; - value: string; - label: string; -}; - function toLabel(value: string) { return value.replace(/_/g, ' '); } @@ -115,6 +101,28 @@ function formatImageCount(count: number | null) { return count.toLocaleString(); } +function formatBytesDecimal(bytes: number | null | undefined) { + if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return null; + const units = ['B', 'kB', 'MB', 'GB', 'TB']; + let value = bytes; + let unitIndex = 0; + while (value >= 1000 && unitIndex < units.length - 1) { + value /= 1000; + unitIndex += 1; + } + const formatted = value >= 100 ? Math.round(value).toString() : value.toFixed(1); + const trimmed = formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted; + return `${trimmed} ${units[unitIndex]}`; +} + +function taskBadgeClass(task: string | null): string { + if (!task) return styles.badgeOther; + if (task.includes('classif')) return styles.badgeClassification; + if (task.includes('detect')) return styles.badgeDetection; + if (task.includes('segment')) return styles.badgeSegmentation; + return styles.badgeOther; +} + function getFilterValues( datasets: import('../../lib/datasets').Dataset[], field: keyof import('../../lib/datasets').Dataset @@ -128,106 +136,40 @@ function getFilterValues( return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b)); } -function MultiSelectDropdown({ +function CheckboxFilterGroup({ label, options, selected, onToggle, formatOption = (value: string) => value.replace(/_/g, ' '), + counts, }: { label: string; options: string[]; selected: string[]; onToggle: (value: string) => void; formatOption?: (value: string) => string; + counts: Record; }) { - const [open, setOpen] = useState(false); - const [focusedIndex, setFocusedIndex] = useState(0); - - useEffect(() => { - const handler = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.closest(`[data-dropdown=\"${label}\"]`)) setOpen(false); - }; - document.addEventListener('click', handler); - return () => document.removeEventListener('click', handler); - }, [label]); - - const onKeyDown = (event: React.KeyboardEvent) => { - if (!open) { - if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { - event.preventDefault(); - setOpen(true); - } - return; - } - - switch (event.key) { - case 'Escape': - event.preventDefault(); - setOpen(false); - break; - case 'ArrowDown': - event.preventDefault(); - setFocusedIndex((index) => (index + 1) % options.length); - break; - case 'ArrowUp': - event.preventDefault(); - setFocusedIndex((index) => (index - 1 + options.length) % options.length); - break; - case 'Enter': - case ' ': - event.preventDefault(); - if (options[focusedIndex] != null) onToggle(options[focusedIndex]); - break; - default: - break; - } - }; - return ( -
- - - {open && ( -
- {options.map((option, index) => { - const isSelected = selected.includes(option); - return ( - - ); - })} -
- )} +
+

{label}

+
+ {options.map((option) => ( + + ))} +
); } @@ -239,8 +181,20 @@ function DatasetCard({ dataset: import('../../lib/datasets').Dataset; onOpen: (trigger: HTMLButtonElement) => void; }) { - const { name, machine_learning_task, agricultural_task, num_images, location } = dataset; - const mainTask = machine_learning_task || agricultural_task; + const { + name, + machine_learning_task, + agricultural_task, + num_images, + zip_size_bytes, + augmented_num_images, + augmented_zip_size_bytes, + location, + } = dataset; + const fileSize = formatBytesDecimal(zip_size_bytes); + const augmentedFileSize = formatBytesDecimal(augmented_zip_size_bytes); + const hasAugmented = augmented_num_images != null; + return ( @@ -295,8 +254,6 @@ export default function DatasetBrowserPage() { return next; }, [searchParams]); - const activeFilterCount = DATASET_FILTERS.reduce((count, filter) => count + selections[filter.key].length, 0); - const setSearchParams = useCallback( (updater: (params: URLSearchParams) => URLSearchParams, replace = true) => { const currentSearch = location.search.startsWith('?') @@ -339,17 +296,7 @@ export default function DatasetBrowserPage() { }); }; - const removeFilterValue = (key: string, value: string) => { - setSearchParams((prev) => { - const current = prev.getAll(key); - const rest = current.filter((v) => v !== value); - const nextParams = new URLSearchParams(prev); - nextParams.delete(key); - rest.forEach((v) => nextParams.append(key, v)); - return nextParams; - }); - }; - + const activeFilterCount = DATASET_FILTERS.reduce((count, filter) => count + selections[filter.key].length, 0); const hasActiveFilters = Boolean(qDeferred || activeFilterCount); const clearFilters = () => { @@ -358,11 +305,30 @@ export default function DatasetBrowserPage() { }; const safeData = Array.isArray(data) ? data : []; + const stats = useMemo(() => computeDatasetStats(safeData), [safeData]); const filterOptions = useMemo( () => Object.fromEntries(DATASET_FILTERS.map((filter) => [filter.key, getFilterValues(safeData, filter.field)])) as Record, [safeData] ); + const filterCounts = useMemo( + () => + Object.fromEntries( + DATASET_FILTERS.map((filter) => [ + filter.key, + Object.fromEntries( + filterOptions[filter.key].map((value) => [ + value, + safeData.filter((dataset) => { + const fieldValue = dataset[filter.field] as string | string[] | null | undefined; + return Array.isArray(fieldValue) ? fieldValue.includes(value) : fieldValue === value; + }).length, + ]) + ), + ]) + ) as Record>, + [safeData, filterOptions] + ); const filtered = useMemo( () => @@ -377,28 +343,23 @@ export default function DatasetBrowserPage() { [safeData, qDeferred, selections] ); - const taskTypes = filterOptions.ml_task.length; - const INITIAL_SHOW = 60; const [showCount, setShowCount] = useState(INITIAL_SHOW); const [selectedDatasetName, setSelectedDatasetName] = useState(null); + + // Supports deep links like /datasets?dataset= (e.g. from the leaderboard's + // "view dataset" link) by opening that dataset's modal once its data has loaded. + const datasetParam = searchParams.get('dataset'); + useEffect(() => { + if (datasetParam) setSelectedDatasetName(datasetParam); + }, [datasetParam]); + const displayed = useMemo(() => filtered.slice(0, showCount), [filtered, showCount]); const hasMore = filtered.length > showCount; const selectedDataset = useMemo( () => safeData.find((dataset) => dataset.name === selectedDatasetName) ?? null, [safeData, selectedDatasetName] ); - const topLevelDatasetCount = useMemo( - () => safeData.filter((dataset) => !dataset.parent_dataset).length, - [safeData] - ); - const totalImageCount = useMemo( - () => - safeData - .filter((dataset) => !dataset.parent_dataset && !dataset.name.startsWith('iNatAg-mini')) - .reduce((sum, dataset) => sum + (dataset.num_images ?? 0), 0), - [safeData] - ); const openDataset = useCallback((datasetName: string, trigger: HTMLButtonElement) => { selectedTriggerRef.current = trigger; @@ -418,20 +379,6 @@ export default function DatasetBrowserPage() { setShowCount(INITIAL_SHOW); }, [qDeferred, activeFilterCount]); - const activeFilterChips = useMemo(() => { - const list: ActiveFilterChip[] = []; - DATASET_FILTERS.forEach((filter) => { - selections[filter.key].forEach((value) => { - list.push({ - key: filter.key, - value, - label: filter.formatOption ? filter.formatOption(value) : value, - }); - }); - }); - return list; - }, [selections]); - return (
@@ -440,125 +387,85 @@ export default function DatasetBrowserPage() { open={selectedDataset != null} onClose={closeDataset} /> -
-
-

AgML Dataset Hub

-

Search agricultural datasets fast.

-

- Filter by task, crop type, environment, location, and platform. Browse the - AgML datasets and the new Hugging Face-backed entries in one place. -

-
-
- {topLevelDatasetCount.toLocaleString()} - datasets -
-
- {totalImageCount.toLocaleString()} - images -
-
- {taskTypes} - task types -
-
-
-
- -
-
- - setQLocal(event.target.value)} - className={styles.searchInput} - /> -
-
- {DATASET_FILTERS.filter((filter) => filter.kind === 'dropdown').map((filter) => ( - toggleMultiFilter(filter.key, value)} - formatOption={filter.formatOption} - /> - ))} + +
+ setQLocal(event.target.value)} + className={styles.searchInput} + /> +
+ {filtered.length.toLocaleString()} datasets + + {stats.imageCount.toLocaleString()} images · {stats.taskTypeCount} task types + + {hasActiveFilters && ( + + )}
-
- -
- {DATASET_FILTERS.filter((filter) => filter.kind === 'chips').map((filter) => ( -
- {filter.chipLabel ?? filter.label} - {filterOptions[filter.key].map((value) => ( - - ))} -
- ))} -
- - {hasActiveFilters && ( -
-
- {activeFilterChips.map((chip) => ( - - ))} -
- -
- )} - -
- {loading &&

Loading datasets…

} - {error &&

Error: {error.message}

} - {!loading && !error && ( - <> - {filtered.length === 0 ? ( -

No datasets match the current filters.

+
+ +
+ + +
+ {loading &&

Loading datasets…

} + {error &&

Error: {error.message}

} + {!loading && !error && ( + <> + {filtered.length === 0 ? ( +

No datasets match the current filters.

+ ) : ( +
+ {displayed.map((dataset) => ( + openDataset(dataset.name, trigger)} /> + ))} +
+ )} + {hasMore && ( +
+ +
+ )} + + )} +
+
); diff --git a/src/pages/index.module.css b/src/pages/index.module.css index d6d6393..6b80255 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -1,81 +1,64 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.hero { - position: relative; - overflow: hidden; - padding: 4.5rem 2rem 4rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 2.5rem; - align-items: center; +.page { background: var(--agml-page-bg); color: var(--agml-text); } -.hero::before, -.hero::after { - content: ''; - position: absolute; - border-radius: 999px; - pointer-events: none; -} - -.hero::before { - width: 420px; - height: 420px; - top: -120px; - left: -120px; - background: radial-gradient(circle at top, rgba(44, 133, 90, 0.45), transparent 70%); -} - -.hero::after { - width: 340px; - height: 340px; - bottom: -140px; - right: -90px; - background: radial-gradient(circle at bottom, var(--agml-warning-soft), transparent 70%); +.hero { + padding: 6rem 1.5rem 4rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + border-bottom: 1px solid var(--agml-border); } .heroContent { - position: relative; - z-index: 1; - max-width: 520px; + display: flex; + flex-direction: column; + align-items: center; + max-width: 760px; } .kicker { text-transform: uppercase; - letter-spacing: 0.2em; + letter-spacing: 0.08em; font-size: 0.75rem; - color: var(--agml-muted); - margin: 0 0 0.8rem; + font-family: var(--ifm-code-font-family); font-weight: 600; + color: var(--ifm-color-primary); + margin: 0 0 1.1rem; } .heroTitle { - font-size: clamp(2.4rem, 3vw, 3.6rem); - margin-bottom: 1rem; + font-size: clamp(2.2rem, 2.8vw, 2.75rem); + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.15; + margin: 0 0 1.25rem; color: var(--agml-text); } .heroSubtitle { - font-size: 1.05rem; + margin: 0 0 2.25rem; + font-size: 1rem; + line-height: 1.6; color: var(--agml-muted); - margin: 0 0 1.8rem; + max-width: 620px; } .heroActions { display: flex; flex-wrap: wrap; - gap: 1rem; + gap: 0.75rem; + justify-content: center; } .primaryButton, .secondaryButton { - border-radius: 999px; - padding: 0.75rem 1.6rem; + border-radius: 7px; + padding: 0.75rem 1.4rem; + font-family: var(--ifm-code-font-family); + font-size: 0.85rem; font-weight: 600; text-decoration: none; display: inline-flex; @@ -84,122 +67,197 @@ } .primaryButton { - background: var(--agml-text); - color: #fff; - box-shadow: var(--agml-shadow); + background: var(--ifm-color-primary); + color: var(--agml-page-bg); } .secondaryButton { border: 1px solid var(--agml-border-strong); color: var(--agml-text); - background: var(--agml-surface); + background: transparent; } -.heroVisual { - position: relative; - z-index: 1; - display: flex; - justify-content: center; +.secondaryButton:hover { + background: var(--agml-surface-soft); + color: var(--agml-text); + text-decoration: none; } -.heroVisual img { - max-width: 420px; - width: 100%; - border-radius: 24px; - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow-strong); - background: var(--agml-surface-strong); +.sectionLabel { + font-family: var(--ifm-code-font-family); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--agml-muted); + margin: 0 0 0.9rem; } -.main { - padding: 3.5rem 2rem 4rem; - max-width: 1100px; +.samplesSection { + padding: 3.5rem 1.5rem; + max-width: 1080px; margin: 0 auto; +} + +.samplesGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +.sampleTile { display: flex; flex-direction: column; - gap: 2.5rem; + gap: 0.5rem; } -.highlightGrid { +.sampleImage { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + border-radius: 10px; + border: 1px solid var(--agml-border); + background: var(--agml-surface-soft); +} + +.sampleCaption { + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; + color: var(--agml-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statsRow { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 1.5rem; + grid-template-columns: repeat(3, 1fr); + border-top: 1px solid var(--agml-border); + border-bottom: 1px solid var(--agml-border); } -.highlightCard { - padding: 1.5rem; - border-radius: 20px; - background: var(--agml-surface-strong); - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); +.statBlock { + padding: 2rem 1.5rem; + text-align: center; + border-right: 1px solid var(--agml-border); } -.highlightCard h2 { - margin-top: 0; - color: var(--agml-text); +.statBlock:last-child { + border-right: none; } -.highlightCard p { - margin-bottom: 0; +.statValue { + display: block; + font-family: var(--ifm-code-font-family); + font-size: 2rem; + font-weight: 700; + color: var(--ifm-color-primary); +} + +.statLabel { + display: block; + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; color: var(--agml-muted); + margin-top: 0.4rem; } -.callout { - display: flex; - flex-wrap: wrap; - gap: 1.5rem; - align-items: center; - justify-content: space-between; - padding: 1.8rem 2rem; - border-radius: 24px; - background: var(--agml-surface); - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); - color: var(--agml-text); +.featuresSection { + padding: 4rem 1.5rem; + max-width: 1080px; + margin: 0 auto; } -.mapSection { +.featuresGrid { display: grid; - gap: 1.8rem; - align-items: center; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; } -.mapImage { - width: 100%; - border-radius: 20px; - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); +.featureCard { background: var(--agml-surface-strong); + border: 1px solid var(--agml-border); + border-radius: 10px; + padding: 1.4rem; + display: flex; + flex-direction: column; + gap: 0.6rem; } -:global(html[data-theme='dark']) .hero::before { - opacity: 0.22; +.featureTag { + margin: 0; + font-family: var(--ifm-code-font-family); + font-size: 0.7rem; + color: var(--ifm-color-primary); } -:global(html[data-theme='dark']) .hero::after { - opacity: 0.18; +.featureTitle { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--agml-text); } -:global(html[data-theme='dark']) .primaryButton { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); - box-shadow: none; - border: 1px solid var(--agml-accent-border); +.featureBody { + margin: 0; + font-size: 0.85rem; + line-height: 1.55; + color: var(--agml-muted); } -:global(html[data-theme='dark']) .secondaryButton { - background: transparent; - color: var(--ifm-color-primary); - border-color: var(--agml-accent-border); +.calloutSection { + padding: 0 1.5rem 4.5rem; + max-width: 1080px; + margin: 0 auto; +} + +.callout { + background: var(--agml-surface-soft); + border: 1px solid var(--agml-border); + border-radius: 10px; + padding: 1.75rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + flex-wrap: wrap; +} + +.calloutTitle { + margin: 0 0 0.4rem; + font-size: 1rem; + font-weight: 600; + color: var(--agml-text); +} + +.calloutCode { + margin: 0; + font-family: var(--ifm-code-font-family); + font-size: 0.82rem; + color: var(--agml-muted); } @media (max-width: 996px) { - .hero { - padding: 3.5rem 1.5rem 3rem; + .samplesGrid { + grid-template-columns: repeat(2, 1fr); + } + + .featuresGrid { + grid-template-columns: 1fr; + } + + .statsRow { + grid-template-columns: 1fr; + } + + .statBlock { + border-right: none; + border-bottom: 1px solid var(--agml-border); } - .main { - padding: 3rem 1.5rem 3.5rem; + .statBlock:last-child { + border-bottom: none; } } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1c73f59..20f2cdd 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,5 @@ import type {ReactNode} from 'react'; +import { useMemo } from 'react'; import Link from '@docusaurus/Link'; import useBaseUrl from '@docusaurus/useBaseUrl'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; @@ -6,8 +7,36 @@ import Layout from '@theme/Layout'; import Heading from '@theme/Heading'; import styles from './index.module.css'; +import { computeDatasetStats, useDatasets } from '../lib/datasets'; -function HomepageHeader() { +// First 4 real datasets (by name) that ship a static sample image — hardcoded rather than +// picked live from useDatasets() so the homepage doesn't depend on the full dataset fetch. +const SAMPLE_IMAGES = [ + { name: 'almond_bloom_2023', path: '/img/agml/sample_images/almond_bloom_2023_examples.webp' }, + { name: 'almond_harvest_2021', path: '/img/agml/sample_images/almond_harvest_2021_examples.webp' }, + { name: 'apple_detection_drone_brazil', path: '/img/agml/sample_images/apple_detection_drone_brazil_examples.webp' }, + { name: 'apple_detection_spain', path: '/img/agml/sample_images/apple_detection_spain_examples.webp' }, +]; + +const FEATURES = [ + { + tag: 'Search', + title: 'Dataset-first workflow', + body: 'Browse thousands of public datasets, filter by task, and jump directly into training with the AgML data loader.', + }, + { + tag: 'Train', + title: 'TensorFlow + PyTorch ready', + body: 'Export loaders to native TensorFlow or PyTorch pipelines without rewriting your data preprocessing logic.', + }, + { + tag: 'Coverage', + title: 'Global coverage', + body: 'From crop disease to detection and segmentation, AgML catalogs datasets spanning continents, crops, and sensor modalities.', + }, +]; + +function HomepageHero() { const {siteConfig} = useDocusaurusContext(); return (
@@ -21,78 +50,95 @@ function HomepageHeader() { tasks and modalities. Start with a dataset, scale to a full pipeline.

- - Read the docs + + Search datasets - - Dataset search + + Browse leaderboard
-
- AgML framework diagram -
); } +function SampleImagery() { + return ( +
+

Sample imagery

+
+ {SAMPLE_IMAGES.map((sample) => ( +
+ {`${sample.name} + {sample.name} +
+ ))} +
+
+ ); +} + +function StatsRow() { + const { data } = useDatasets(); + const stats = useMemo(() => computeDatasetStats(data), [data]); + + return ( +
+
+ {stats.datasetCount.toLocaleString()} + Datasets indexed +
+
+ {stats.imageCount.toLocaleString()} + Labeled images +
+
+ {stats.taskTypeCount.toLocaleString()} + CV task types +
+
+ ); +} + export default function Home(): ReactNode { return ( - -
-
-
-

Dataset-first workflow

-

- Browse thousands of public datasets, filter by task, and jump directly into training - with the AgML data loader. -

-
-
-

TensorFlow + PyTorch ready

-

- Export loaders to native TensorFlow or PyTorch pipelines without rewriting your data - preprocessing logic. -

-
-
-

Global coverage

-

- From crop disease to detection and segmentation, AgML catalogs datasets spanning - continents, crops, and sensor modalities. -

-
-
+
+ + + -
-
-

Find the right dataset in minutes

-

- Use the new dataset search page to filter by task, platform, and modality, then dive - into detailed dataset docs in one click. -

+
+

What AgML offers

+
+ {FEATURES.map((feature) => ( +
+

{feature.tag}

+

{feature.title}

+

{feature.body}

+
+ ))}
- - Explore datasets -
-
-
-

See AgML on the map

-

- AgML datasets span a global network of agricultural research efforts. Track coverage - and discover new sources from every region. -

+
+
+
+

Load any dataset in one line

+

+ loader = agml.data.AgMLDataLoader('apple_flower_segmentation') +

+
+ + Read the docs +
- AgML dataset world map
-
+
); } diff --git a/src/pages/leaderboard/index.module.css b/src/pages/leaderboard/index.module.css index a207203..90516c9 100644 --- a/src/pages/leaderboard/index.module.css +++ b/src/pages/leaderboard/index.module.css @@ -1,128 +1,185 @@ .page { - position: relative; - overflow: visible; - padding: 3.5rem 1.5rem 4rem; background: var(--agml-page-bg); color: var(--agml-text); + min-height: calc(100vh - 60px); + display: flex; + flex-direction: column; } -.hero { - position: relative; - z-index: 1; - max-width: 1400px; - margin: 0 auto 2.5rem; - padding: 2.5rem 2.5rem 2rem; - border-radius: 28px; - border: 1px solid var(--agml-border); +.toolbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.9rem 1.5rem; + border-bottom: 1px solid var(--agml-border); background: var(--agml-surface); - box-shadow: var(--agml-shadow-strong); } -.heroContent { +.searchInput { + flex: 1; + max-width: 480px; + border-radius: 6px; + border: 1px solid var(--agml-border-strong); + background: var(--agml-surface-strong); + padding: 0.55rem 0.8rem; + font-family: var(--ifm-code-font-family); + font-size: 0.85rem; + color: var(--agml-text); +} + +.searchInput:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--agml-accent-soft); +} + +.toolbarRight { + margin-left: auto; display: flex; - flex-direction: column; + align-items: center; gap: 1rem; } -.heroTag { - text-transform: uppercase; - letter-spacing: 0.2em; - font-size: 0.75rem; - font-weight: 600; +.resultCount { + font-family: var(--ifm-code-font-family); + font-size: 0.78rem; color: var(--agml-muted); - margin: 0; + white-space: nowrap; } -.heroTitle { - font-size: clamp(2.2rem, 2.8vw, 3.4rem); - margin: 0; +.clearButton { + border: 1px solid var(--agml-border-strong); + background: transparent; + border-radius: 999px; + padding: 0.4rem 0.9rem; + font-size: 0.8rem; color: var(--agml-text); + cursor: pointer; + white-space: nowrap; } -.heroSubtitle { - margin: 0; - font-size: 1.05rem; +.body { + flex: 1; + display: flex; + min-height: 0; +} + +.sidebar { + width: 240px; + flex-shrink: 0; + border-right: 1px solid var(--agml-border); + background: var(--agml-surface-soft); + padding: 1.25rem 1.1rem; + overflow-y: auto; +} + +.filterGroup { + margin-bottom: 1.6rem; +} + +.filterGroupLabel { + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; color: var(--agml-muted); - max-width: 720px; + margin: 0 0 0.7rem; } -.controls { - position: relative; - z-index: 30; - max-width: 1400px; - margin: 0 auto 1.5rem; +.filterGroupOptions { display: flex; - flex-wrap: wrap; - gap: 1.2rem; - align-items: center; - justify-content: space-between; - padding: 1.5rem 2rem; - border-radius: 22px; - background: var(--agml-surface); - border: 1px solid var(--agml-border); - box-shadow: var(--agml-shadow); + flex-direction: column; + gap: 0.55rem; } -.dropdownRow { +.checkboxRow { display: flex; - gap: 1rem; - flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + cursor: pointer; + color: var(--agml-text); } -.clearButton { - border: 1px solid var(--agml-border-strong); - background: transparent; - border-radius: 999px; - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: var(--agml-text); +.checkbox { + width: 15px; + height: 15px; + accent-color: var(--ifm-color-primary); cursor: pointer; } .results { - position: relative; - z-index: 1; - max-width: 1400px; - margin: 0 auto; + flex: 1; + overflow: auto; + padding: 1.5rem; } .status { color: var(--agml-muted); } +.tableWrap { + border: 1px solid var(--agml-border); + border-radius: 8px; + overflow-x: auto; +} + .leaderboardTable { width: 100%; + min-width: 1080px; border-collapse: collapse; - background: var(--agml-surface-strong); - border-radius: 20px; - overflow: hidden; - box-shadow: var(--agml-shadow); + font-size: 0.85rem; +} + +.leaderboardTable thead, +.leaderboardTable tbody tr { + background: transparent; +} + +.leaderboardTable thead tr { + border-bottom: none; +} + +.leaderboardTable tr:nth-child(2n) { + background-color: transparent; } .leaderboardTable th, .leaderboardTable td { text-align: left; - padding: 0.85rem 1.1rem; + padding: 0.65rem 0.85rem; + border: none; border-bottom: 1px solid var(--agml-border); - font-size: 0.92rem; - color: var(--agml-text); } .leaderboardTable th { - font-size: 0.72rem; + font-family: var(--ifm-code-font-family); + font-size: 0.66rem; text-transform: uppercase; - letter-spacing: 0.12em; + letter-spacing: 0.05em; color: var(--agml-muted); - background: var(--agml-surface); + background: var(--agml-surface-soft); } .leaderboardTable tbody tr:last-child td { border-bottom: none; } -.datasetsCell { - color: var(--agml-muted); - font-size: 0.85rem; +.sortHeader { + background: none; + border: none; + padding: 0; + cursor: pointer; + font-family: inherit; + font-size: inherit; + text-transform: inherit; + letter-spacing: inherit; + color: inherit; +} + +.sortHeaderActive { + color: var(--ifm-color-primary); } .clickableRow { @@ -134,43 +191,72 @@ } .modelName { - font-weight: 600; + font-family: var(--ifm-code-font-family); + font-weight: 500; + color: var(--agml-text); + text-decoration: underline dashed var(--agml-muted); + text-decoration-thickness: 1px; + text-underline-offset: 3px; } -.expandChevron { - margin-left: 0.5rem; - color: var(--agml-muted); - font-size: 0.8rem; +.taskBadge { + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + padding: 0.15rem 0.5rem; + border-radius: 4px; + display: inline-block; + white-space: nowrap; } -.detailsCell { - background: var(--agml-surface); - padding: 0.9rem 1.4rem 1.1rem; +.badgeClassification { + background: var(--agml-badge-classification-bg); + color: var(--agml-badge-classification-fg); } -.detailsTable { - width: 100%; - border-collapse: collapse; +.badgeDetection { + background: var(--agml-badge-detection-bg); + color: var(--agml-badge-detection-fg); } -.detailsTable th, -.detailsTable td { - text-align: left; - padding: 0.5rem 0.8rem; - font-size: 0.85rem; - color: var(--agml-text); - border-bottom: 1px solid var(--agml-border); +.badgeSegmentation { + background: var(--agml-badge-segmentation-bg); + color: var(--agml-badge-segmentation-fg); } -.detailsTable th { - font-size: 0.68rem; - text-transform: uppercase; - letter-spacing: 0.1em; +.badgeOther { + background: var(--agml-badge-other-bg); + color: var(--agml-badge-other-fg); +} + +.metricCell { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 110px; +} + +.metricBarTrack { + flex: 1; + height: 6px; + border-radius: 3px; + background: var(--agml-surface-soft); + overflow: hidden; +} + +.metricBarFill { + height: 100%; + background: var(--ifm-color-primary); +} + +.metricLabel { + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; color: var(--agml-muted); + flex-shrink: 0; } -.detailsTable tbody tr:last-child td { - border-bottom: none; +.metricEmpty { + color: var(--agml-muted); } .pagination { @@ -202,11 +288,13 @@ } @media (max-width: 996px) { - .hero { - padding: 2rem 1.6rem; + .body { + flex-direction: column; } - .controls { - padding: 1.2rem 1.5rem; + .sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--agml-border); } } diff --git a/src/pages/leaderboard/index.tsx b/src/pages/leaderboard/index.tsx index e49c589..29762e5 100644 --- a/src/pages/leaderboard/index.tsx +++ b/src/pages/leaderboard/index.tsx @@ -1,234 +1,327 @@ -import { Fragment, useEffect, useMemo, useState } from 'react'; +import { useDeferredValue, useEffect, useMemo, useState } from 'react'; import Layout from '@theme/Layout'; -import { computeGlobalLeaderboard, formatGlobalResultTypeKey, globalResultTypeKey, useGlobalPerformance } from '../../lib/performance'; +import { + computeGlobalLeaderboard, + useGlobalPerformance, +} from '../../lib/performance'; +import type { GlobalLeaderboardEntry } from '../../lib/performance'; import { MultiSelectDropdown } from '../../components/MultiSelectDropdown'; +import { LeaderboardDetailModal } from '../../components/LeaderboardDetailModal'; import styles from './index.module.css'; const MIN_APPEARANCES = 3; const PAGE_SIZE = 25; +type SortField = 'f1' | 'map' | 'precision' | 'recall'; + function toLabel(value: string) { return value.replace(/_/g, ' '); } +function percentileValue(entry: GlobalLeaderboardEntry, field: SortField) { + switch (field) { + case 'f1': + return entry.avgF1Percentile; + case 'map': + return entry.avgMapPercentile; + case 'precision': + return entry.avgPrecisionPercentile; + case 'recall': + return entry.avgRecallPercentile; + } +} + +function taskBadgeClass(task: string | null): string { + if (!task) return styles.badgeOther; + if (task.includes('classif')) return styles.badgeClassification; + if (task.includes('detect')) return styles.badgeDetection; + if (task.includes('segment')) return styles.badgeSegmentation; + return styles.badgeOther; +} + +function CheckboxFilterGroup({ + label, + options, + selected, + onToggle, + formatOption = (value: string) => value, +}: { + label: string; + options: string[]; + selected: string[]; + onToggle: (value: string) => void; + formatOption?: (value: string) => string; +}) { + return ( +
+

{label}

+
+ {options.map((option) => ( + + ))} +
+
+ ); +} + +function PercentileCell({ value }: { value: number | null }) { + if (value == null) { + return ; + } + return ( +
+
+
+
+ {value.toFixed(0)}th +
+ ); +} + export default function GlobalLeaderboardPage() { const { data: records, loading, error } = useGlobalPerformance(); + const [search, setSearch] = useState(''); + const searchDeferred = useDeferredValue(search); const [cropTypes, setCropTypes] = useState([]); const [mlTasks, setMlTasks] = useState([]); - const [resultTypes, setResultTypes] = useState([]); + const [tuned, setTuned] = useState<('tuned' | 'not-tuned')[]>([]); + const [optimizedValues, setOptimizedValues] = useState<('optimized' | 'not-optimized')[]>([]); const [platforms, setPlatforms] = useState([]); + const [sortBy, setSortBy] = useState('map'); - const { cropTypeOptions, mlTaskOptions, resultTypeOptions, platformOptions } = useMemo(() => { + const { cropTypeOptions, mlTaskOptions, platformOptions } = useMemo(() => { const cropSet = new Set(); const taskSet = new Set(); - const resultTypeSet = new Set(); const platformSet = new Set(); for (const record of records) { record.crop_types?.forEach((crop) => cropSet.add(crop)); if (record.machine_learning_task) taskSet.add(record.machine_learning_task); - const resultTypeKey = globalResultTypeKey(record); - if (resultTypeKey) resultTypeSet.add(resultTypeKey); if (record.platform) platformSet.add(record.platform); } return { cropTypeOptions: Array.from(cropSet).sort((a, b) => a.localeCompare(b)), mlTaskOptions: Array.from(taskSet).sort((a, b) => a.localeCompare(b)), - resultTypeOptions: Array.from(resultTypeSet).sort(), platformOptions: Array.from(platformSet).sort((a, b) => a.localeCompare(b)), }; }, [records]); - const toggleCropType = (value: string) => { - setCropTypes((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); - }; - - const toggleMlTask = (value: string) => { - setMlTasks((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); - }; - - const toggleResultType = (value: string) => { - setResultTypes((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); - }; - - const togglePlatform = (value: string) => { - setPlatforms((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); + const toggleValue = (setter: (updater: (current: T[]) => T[]) => void, value: T) => { + setter((current) => (current.includes(value) ? current.filter((v) => v !== value) : [...current, value])); }; - const hasActiveFilters = cropTypes.length > 0 || mlTasks.length > 0 || resultTypes.length > 0 || platforms.length > 0; + const hasActiveFilters = + cropTypes.length > 0 || mlTasks.length > 0 || tuned.length > 0 || optimizedValues.length > 0 || platforms.length > 0; const clearFilters = () => { setCropTypes([]); setMlTasks([]); - setResultTypes([]); + setTuned([]); + setOptimizedValues([]); setPlatforms([]); }; const leaderboard = useMemo( - () => computeGlobalLeaderboard(records, { cropTypes, mlTasks, resultTypes, platforms, minAppearances: MIN_APPEARANCES }), - [records, cropTypes, mlTasks, resultTypes, platforms] + () => + computeGlobalLeaderboard(records, { + cropTypes, + mlTasks, + tuned, + optimizedValues, + platforms, + minAppearances: MIN_APPEARANCES, + }), + [records, cropTypes, mlTasks, tuned, optimizedValues, platforms] + ); + + const searched = useMemo(() => { + const q = searchDeferred.trim().toLowerCase(); + if (!q) return leaderboard; + return leaderboard.filter( + (entry) => + entry.model.toLowerCase().includes(q) || + (entry.machineLearningTask ?? '').toLowerCase().includes(q) || + entry.datasets.some((dataset) => dataset.toLowerCase().includes(q)) + ); + }, [leaderboard, searchDeferred]); + + const sorted = useMemo( + () => + [...searched].sort((a, b) => (percentileValue(b, sortBy) ?? -1) - (percentileValue(a, sortBy) ?? -1)), + [searched, sortBy] ); const [page, setPage] = useState(1); - useEffect(() => setPage(1), [cropTypes, mlTasks, resultTypes, platforms]); + useEffect(() => setPage(1), [cropTypes, mlTasks, tuned, optimizedValues, platforms, searchDeferred]); - const pageCount = Math.max(1, Math.ceil(leaderboard.length / PAGE_SIZE)); + const pageCount = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE)); const currentPage = Math.min(page, pageCount); const pagedLeaderboard = useMemo( - () => leaderboard.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), - [leaderboard, currentPage] + () => sorted.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), + [sorted, currentPage] ); - const [expandedKeys, setExpandedKeys] = useState>(new Set()); - const toggleExpanded = (key: string) => { - setExpandedKeys((current) => { - const next = new Set(current); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - useEffect(() => setExpandedKeys(new Set()), [cropTypes, mlTasks, resultTypes, platforms, page]); + const [selectedKey, setSelectedKey] = useState(null); + const selectedEntry = useMemo( + () => sorted.find((entry) => `${entry.model}|||${entry.machineLearningTask ?? ''}` === selectedKey) ?? null, + [sorted, selectedKey] + ); + + const sortHeaderClass = (field: SortField) => `${styles.sortHeader} ${sortBy === field ? styles.sortHeaderActive : ''}`; return (
-
-
-

AgML Model Leaderboard

-

Global model performance

-

- Models are ranked by their average percentile across every dataset leaderboard they appear on. - Only models with at least {MIN_APPEARANCES} dataset appearances are included. -

+ setSelectedKey(null)} /> + +
+ setSearch(event.target.value)} + className={styles.searchInput} + /> +
+ {sorted.length.toLocaleString()} models + {hasActiveFilters && ( + + )}
-
- -
-
- - + +
+
- {hasActiveFilters && ( - - )} -
- -
- {loading &&

Loading leaderboard…

} - {error &&

Error: {error.message}

} - {!loading && !error && leaderboard.length === 0 && ( -

- No models have at least {MIN_APPEARANCES} dataset appearances for the current filters. -

- )} - {!loading && !error && leaderboard.length > 0 && ( - <> - - - - - - - - - - - - {pagedLeaderboard.map((entry) => { - const key = `${entry.model}|||${entry.machineLearningTask ?? ''}`; - const isExpanded = expandedKeys.has(key); - return ( - - toggleExpanded(key)} - aria-expanded={isExpanded} - > - - - - - - - {isExpanded && ( - - +
CV-taskModel nameAvg. percentile (mAP@.5)Result typeNumber of results
{entry.machineLearningTask ? toLabel(entry.machineLearningTask) : '—'} - {entry.model} - - {isExpanded ? '▾' : '▸'} - - {entry.averagePercentile.toFixed(1)}{entry.resultType}{entry.appearances}
- - - - - - - - - - - {entry.datasetDetails.map((detail) => ( - - - - - - - ))} - -
DatasetPercentileResult typePlatform
{toLabel(detail.dataset)}{detail.percentile.toFixed(1)} - {detail.variant === 'fine-tuned' - ? 'Fine-tuned' - : detail.variant === 'zero-shot' - ? 'Zero-shot' - : '—'} - {detail.optimized ? ' (optimized)' : ''} - {detail.platform ?? '—'}
+ toggleValue(setOptimizedValues, value as 'optimized' | 'not-optimized')} + formatOption={(value) => (value === 'optimized' ? 'Optimized' : 'Not optimized')} + /> + toggleValue(setPlatforms, value)} formatOption={toLabel} /> + + +
+ {loading &&

Loading leaderboard…

} + {error &&

Error: {error.message}

} + {!loading && !error && sorted.length === 0 && ( +

+ No models have at least {MIN_APPEARANCES} dataset appearances for the current filters. +

+ )} + {!loading && !error && sorted.length > 0 && ( + <> +
+ + + + + + + + + + + + + + + {pagedLeaderboard.map((entry) => { + const key = `${entry.model}|||${entry.machineLearningTask ?? ''}`; + return ( + setSelectedKey(key)}> + + + + + + + + - )} - - ); - })} - -
CV TaskModel + + + + + + + + Result type# Results
+ + {entry.machineLearningTask ? toLabel(entry.machineLearningTask) : 'Unknown'} + + + {entry.model} + + + + + + + + {entry.resultType}{entry.appearances}
- {pageCount > 1 && ( -
- - - Page {currentPage} of {pageCount} - - + ); + })} +
- )} - - )} - + {pageCount > 1 && ( +
+ + + Page {currentPage} of {pageCount} + + +
+ )} + + )} + +
); diff --git a/static/data/performance/MoringaLeafNet_disease_classification.json b/static/data/performance/MoringaLeafNet_disease_classification.json new file mode 100644 index 0000000..67bd0d0 --- /dev/null +++ b/static/data/performance/MoringaLeafNet_disease_classification.json @@ -0,0 +1,77 @@ +[ + { + "timestamp": "2026-07-18T23:17:33.173272+00:00", + "dataset": "Project-AgML/MoringaLeafNet_disease_classification", + "dataset_config": null, + "split": "train", + "model": "openai/clip-vit-base-patch32", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2817, + "device": "cuda", + "inference_time_seconds": 261.95, + "inference_time_seconds_per_image": 0.09299, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.297072, + "precision": 0.46061, + "recall": 0.423695 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:22:23.033141+00:00", + "dataset": "Project-AgML/MoringaLeafNet_disease_classification", + "dataset_config": null, + "split": "train", + "model": "google/siglip-base-patch16-224", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2817, + "device": "cuda", + "inference_time_seconds": 287.08, + "inference_time_seconds_per_image": 0.101911, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.320317, + "precision": 0.505885, + "recall": 0.399747 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:26:39.899754+00:00", + "dataset": "Project-AgML/MoringaLeafNet_disease_classification", + "dataset_config": null, + "split": "train", + "model": "kakaobrain/align-base", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2817, + "device": "cuda", + "inference_time_seconds": 253.68, + "inference_time_seconds_per_image": 0.090052, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.130136, + "precision": 0.407792, + "recall": 0.254646 + }, + "notes": "" + } +] \ No newline at end of file diff --git a/static/data/performance/ash_gourd_disease_classification.json b/static/data/performance/ash_gourd_disease_classification.json new file mode 100644 index 0000000..a21ff94 --- /dev/null +++ b/static/data/performance/ash_gourd_disease_classification.json @@ -0,0 +1,77 @@ +[ + { + "timestamp": "2026-07-18T23:27:54.133075+00:00", + "dataset": "Project-AgML/ash_gourd_disease_classification", + "dataset_config": null, + "split": "train", + "model": "openai/clip-vit-base-patch32", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2676, + "device": "cuda", + "inference_time_seconds": 70.37, + "inference_time_seconds_per_image": 0.026298, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.152805, + "precision": 0.139378, + "recall": 0.199623 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:29:08.172946+00:00", + "dataset": "Project-AgML/ash_gourd_disease_classification", + "dataset_config": null, + "split": "train", + "model": "google/siglip-base-patch16-224", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2676, + "device": "cuda", + "inference_time_seconds": 71.08, + "inference_time_seconds_per_image": 0.026562, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.215341, + "precision": 0.175789, + "recall": 0.283302 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:30:31.504295+00:00", + "dataset": "Project-AgML/ash_gourd_disease_classification", + "dataset_config": null, + "split": "train", + "model": "kakaobrain/align-base", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 2676, + "device": "cuda", + "inference_time_seconds": 80.02, + "inference_time_seconds_per_image": 0.029902, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.086037, + "precision": 0.207444, + "recall": 0.22084 + }, + "notes": "" + } +] \ No newline at end of file diff --git a/static/data/performance/cotton_leaf_disease_classification.json b/static/data/performance/cotton_leaf_disease_classification.json new file mode 100644 index 0000000..c94ff30 --- /dev/null +++ b/static/data/performance/cotton_leaf_disease_classification.json @@ -0,0 +1,77 @@ +[ + { + "timestamp": "2026-07-18T23:11:28.749699+00:00", + "dataset": "Project-AgML/cotton_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "openai/clip-vit-base-patch32", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1373, + "device": "cuda", + "inference_time_seconds": 6.36, + "inference_time_seconds_per_image": 0.004634, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.110828, + "precision": 0.109944, + "recall": 0.241157 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:11:38.074861+00:00", + "dataset": "Project-AgML/cotton_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "google/siglip-base-patch16-224", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1373, + "device": "cuda", + "inference_time_seconds": 6.62, + "inference_time_seconds_per_image": 0.004822, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.313517, + "precision": 0.679998, + "recall": 0.410273 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:11:50.448026+00:00", + "dataset": "Project-AgML/cotton_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "kakaobrain/align-base", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1373, + "device": "cuda", + "inference_time_seconds": 9.24, + "inference_time_seconds_per_image": 0.00673, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.100794, + "precision": 0.108849, + "recall": 0.224622 + }, + "notes": "" + } +] \ No newline at end of file diff --git a/static/data/performance/eggplant_leaf_disease_classification.json b/static/data/performance/eggplant_leaf_disease_classification.json new file mode 100644 index 0000000..684b269 --- /dev/null +++ b/static/data/performance/eggplant_leaf_disease_classification.json @@ -0,0 +1,77 @@ +[ + { + "timestamp": "2026-07-18T23:12:14.210533+00:00", + "dataset": "Project-AgML/eggplant_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "openai/clip-vit-base-patch32", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 3116, + "device": "cuda", + "inference_time_seconds": 20.02, + "inference_time_seconds_per_image": 0.006425, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.252379, + "precision": 0.318532, + "recall": 0.301476 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:12:38.747166+00:00", + "dataset": "Project-AgML/eggplant_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "google/siglip-base-patch16-224", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 3116, + "device": "cuda", + "inference_time_seconds": 21.59, + "inference_time_seconds_per_image": 0.006927, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.299814, + "precision": 0.388911, + "recall": 0.362106 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:13:07.258348+00:00", + "dataset": "Project-AgML/eggplant_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "kakaobrain/align-base", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 3116, + "device": "cuda", + "inference_time_seconds": 25.38, + "inference_time_seconds_per_image": 0.008146, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.239863, + "precision": 0.290847, + "recall": 0.307375 + }, + "notes": "" + } +] \ No newline at end of file diff --git a/static/data/performance/global.json b/static/data/performance/global.json new file mode 100644 index 0000000..e40976d --- /dev/null +++ b/static/data/performance/global.json @@ -0,0 +1,371 @@ +[ + { + "model": "google/siglip-base-patch16-224", + "dataset": "MoringaLeafNet_disease_classification", + "percentiles": { + "f1": 100, + "map": null, + "precision": 100, + "recall": 50 + }, + "scores": { + "f1": 0.320317, + "map": null, + "precision": 0.505885, + "recall": 0.399747 + }, + "crop_types": [ + "moringa" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "openai/clip-vit-base-patch32", + "dataset": "MoringaLeafNet_disease_classification", + "percentiles": { + "f1": 50, + "map": null, + "precision": 50, + "recall": 100 + }, + "scores": { + "f1": 0.297072, + "map": null, + "precision": 0.46061, + "recall": 0.423695 + }, + "crop_types": [ + "moringa" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "kakaobrain/align-base", + "dataset": "MoringaLeafNet_disease_classification", + "percentiles": { + "f1": 0, + "map": null, + "precision": 0, + "recall": 0 + }, + "scores": { + "f1": 0.130136, + "map": null, + "precision": 0.407792, + "recall": 0.254646 + }, + "crop_types": [ + "moringa" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "google/siglip-base-patch16-224", + "dataset": "ash_gourd_disease_classification", + "percentiles": { + "f1": 100, + "map": null, + "precision": 50, + "recall": 100 + }, + "scores": { + "f1": 0.215341, + "map": null, + "precision": 0.175789, + "recall": 0.283302 + }, + "crop_types": [ + "ash gourd" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "openai/clip-vit-base-patch32", + "dataset": "ash_gourd_disease_classification", + "percentiles": { + "f1": 50, + "map": null, + "precision": 0, + "recall": 0 + }, + "scores": { + "f1": 0.152805, + "map": null, + "precision": 0.139378, + "recall": 0.199623 + }, + "crop_types": [ + "ash gourd" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "kakaobrain/align-base", + "dataset": "ash_gourd_disease_classification", + "percentiles": { + "f1": 0, + "map": null, + "precision": 100, + "recall": 50 + }, + "scores": { + "f1": 0.086037, + "map": null, + "precision": 0.207444, + "recall": 0.22084 + }, + "crop_types": [ + "ash gourd" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "google/siglip-base-patch16-224", + "dataset": "cotton_leaf_disease_classification", + "percentiles": { + "f1": 100, + "map": null, + "precision": 100, + "recall": 100 + }, + "scores": { + "f1": 0.313517, + "map": null, + "precision": 0.679998, + "recall": 0.410273 + }, + "crop_types": [ + "cotton" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "openai/clip-vit-base-patch32", + "dataset": "cotton_leaf_disease_classification", + "percentiles": { + "f1": 50, + "map": null, + "precision": 50, + "recall": 50 + }, + "scores": { + "f1": 0.110828, + "map": null, + "precision": 0.109944, + "recall": 0.241157 + }, + "crop_types": [ + "cotton" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "kakaobrain/align-base", + "dataset": "cotton_leaf_disease_classification", + "percentiles": { + "f1": 0, + "map": null, + "precision": 0, + "recall": 0 + }, + "scores": { + "f1": 0.100794, + "map": null, + "precision": 0.108849, + "recall": 0.224622 + }, + "crop_types": [ + "cotton" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "google/siglip-base-patch16-224", + "dataset": "eggplant_leaf_disease_classification", + "percentiles": { + "f1": 100, + "map": null, + "precision": 100, + "recall": 100 + }, + "scores": { + "f1": 0.299814, + "map": null, + "precision": 0.388911, + "recall": 0.362106 + }, + "crop_types": [ + "eggplant" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "openai/clip-vit-base-patch32", + "dataset": "eggplant_leaf_disease_classification", + "percentiles": { + "f1": 50, + "map": null, + "precision": 50, + "recall": 0 + }, + "scores": { + "f1": 0.252379, + "map": null, + "precision": 0.318532, + "recall": 0.301476 + }, + "crop_types": [ + "eggplant" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "kakaobrain/align-base", + "dataset": "eggplant_leaf_disease_classification", + "percentiles": { + "f1": 0, + "map": null, + "precision": 0, + "recall": 50 + }, + "scores": { + "f1": 0.239863, + "map": null, + "precision": 0.290847, + "recall": 0.307375 + }, + "crop_types": [ + "eggplant" + ], + "machine_learning_task": "image_classification", + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "kakaobrain/align-base", + "dataset": "groundnut_leaf_disease_classification", + "percentiles": { + "f1": 100, + "map": null, + "precision": 50, + "recall": 100 + }, + "scores": { + "f1": 0.157617, + "map": null, + "precision": 0.120098, + "recall": 0.229333 + }, + "crop_types": null, + "machine_learning_task": null, + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "google/siglip-base-patch16-224", + "dataset": "groundnut_leaf_disease_classification", + "percentiles": { + "f1": 50, + "map": null, + "precision": 100, + "recall": 50 + }, + "scores": { + "f1": 0.145332, + "map": null, + "precision": 0.127236, + "recall": 0.219556 + }, + "crop_types": null, + "machine_learning_task": null, + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + }, + { + "model": "openai/clip-vit-base-patch32", + "dataset": "groundnut_leaf_disease_classification", + "percentiles": { + "f1": 0, + "map": null, + "precision": 0, + "recall": 0 + }, + "scores": { + "f1": 0.082803, + "map": null, + "precision": 0.05224, + "recall": 0.199556 + }, + "crop_types": null, + "machine_learning_task": null, + "variant": "zero-shot", + "optimized": true, + "platform": "cuda", + "splitBreakdown": "-/100/-", + "datasetConfig": "train" + } +] \ No newline at end of file diff --git a/static/data/performance/groundnut_leaf_disease_classification.json b/static/data/performance/groundnut_leaf_disease_classification.json new file mode 100644 index 0000000..e677269 --- /dev/null +++ b/static/data/performance/groundnut_leaf_disease_classification.json @@ -0,0 +1,77 @@ +[ + { + "timestamp": "2026-07-18T23:00:41.273137+00:00", + "dataset": "Project-AgML/groundnut_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "openai/clip-vit-base-patch32", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1720, + "device": "cuda", + "inference_time_seconds": 364.31, + "inference_time_seconds_per_image": 0.21181, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.082803, + "precision": 0.05224, + "recall": 0.199556 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:05:56.625342+00:00", + "dataset": "Project-AgML/groundnut_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "google/siglip-base-patch16-224", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1720, + "device": "cuda", + "inference_time_seconds": 312.06, + "inference_time_seconds_per_image": 0.181432, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.145332, + "precision": 0.127236, + "recall": 0.219556 + }, + "notes": "" + }, + { + "timestamp": "2026-07-18T23:11:18.750341+00:00", + "dataset": "Project-AgML/groundnut_leaf_disease_classification", + "dataset_config": null, + "split": "train", + "model": "kakaobrain/align-base", + "task": "classification", + "result_type": "zero-shot", + "optimized": "no", + "train_pct": 0.0, + "test_pct": 100.0, + "val_pct": 0.0, + "num_samples": 1720, + "device": "cuda", + "inference_time_seconds": 318.51, + "inference_time_seconds_per_image": 0.185183, + "train_time_seconds": 0.0, + "train_time_seconds_per_image": 0.0, + "metrics": { + "f1": 0.157617, + "precision": 0.120098, + "recall": 0.229333 + }, + "notes": "" + } +] \ No newline at end of file diff --git a/static/data/performance/index.json b/static/data/performance/index.json new file mode 100644 index 0000000..16c7ae3 --- /dev/null +++ b/static/data/performance/index.json @@ -0,0 +1,7 @@ +[ + "MoringaLeafNet_disease_classification", + "ash_gourd_disease_classification", + "cotton_leaf_disease_classification", + "eggplant_leaf_disease_classification", + "groundnut_leaf_disease_classification" +] \ No newline at end of file From f49c8940e34e6e209ad52feefe3713a88d5a8563 Mon Sep 17 00:00:00 2001 From: Jared Smith Date: Mon, 20 Jul 2026 10:03:11 -0700 Subject: [PATCH 5/8] more ui updates --- .gitignore | 2 + docusaurus.config.ts | 8 +- scripts/generate-datasets.mjs | 14 +- sidebars.ts | 19 +- .../DatasetMetadataModal.module.css | 423 ++++++++------- src/components/DatasetMetadataModal.tsx | 506 +++++++++--------- .../LeaderboardDetailModal.module.css | 85 +-- src/components/LeaderboardDetailModal.tsx | 164 +++--- src/components/MultiSelectDropdown.module.css | 48 +- src/css/custom.css | 126 ++++- src/lib/performance.ts | 16 +- src/pages/leaderboard/index.module.css | 90 +++- src/pages/leaderboard/index.tsx | 134 ++--- static/data/hf_datasets.json | 2 +- static/data/performance/global.json | 60 +-- 15 files changed, 971 insertions(+), 726 deletions(-) diff --git a/.gitignore b/.gitignore index b2d6de3..d322cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.external \ No newline at end of file diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 81e2154..02cb3fb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -61,19 +61,15 @@ const config: Config = { }, navbar: { title: 'AgML', - logo: { - alt: 'AgML Logo', - src: 'img/agml/agml-logo-wide.png', - }, items: [ + {to: '/datasets', label: 'Datasets', position: 'left'}, + {to: '/leaderboard', label: 'Leaderboard', position: 'left'}, { type: 'docSidebar', sidebarId: 'agmlSidebar', position: 'left', label: 'Docs', }, - {to: '/datasets', label: 'Dataset Search', position: 'left'}, - {to: '/leaderboard', label: 'Leaderboard', position: 'left'}, { href: 'https://github.com/Project-AgML/AgML', label: 'GitHub', diff --git a/scripts/generate-datasets.mjs b/scripts/generate-datasets.mjs index ad34f5e..9e41ef2 100644 --- a/scripts/generate-datasets.mjs +++ b/scripts/generate-datasets.mjs @@ -122,6 +122,13 @@ function buildSplitBreakdown(entry, finetune) { return `${toShare(trainSamples)}/${toShare(testSamples)}/${toShare(valSamples)}`; } +// The result set encodes optimized as the string "yes" / "no" (not a boolean) — a finetune +// object's own optimized field, if present, may already be boolean, so both forms are accepted. +function isOptimized(value) { + if (typeof value === 'string') return value.trim().toLowerCase() === 'yes'; + return Boolean(value); +} + function makeLeaderboardRow(entry, metricKey, variant) { const finetune = entry.finetune; return { @@ -133,10 +140,13 @@ function makeLeaderboardRow(entry, metricKey, variant) { submitted_by: null, link: null, notes: variant === 'fine-tuned' ? buildFinetuneNote(finetune) : buildRunNote(entry), - optimized: Boolean(entry.optimized) || (finetune != null && typeof finetune === 'object' && Boolean(finetune.optimized)), + optimized: isOptimized(entry.optimized) || (finetune != null && typeof finetune === 'object' && isOptimized(finetune.optimized)), platform: typeof entry.device === 'string' && entry.device.trim() ? entry.device.trim() : null, splitBreakdown: buildSplitBreakdown(entry, finetune), - datasetConfig: (typeof entry.dataset_config === 'string' && entry.dataset_config.trim()) || (typeof entry.split === 'string' && entry.split.trim()) || null, + // dataset_config is the result set's own config field (raw / augmented) — no fallback to + // `split`, which is a different concept (the run's data split, e.g. "train"). Absent config + // means the run used the dataset's raw (unaugmented) form. + datasetConfig: typeof entry.dataset_config === 'string' && entry.dataset_config.trim() ? entry.dataset_config.trim() : 'raw', }; } diff --git a/sidebars.ts b/sidebars.ts index a4caefd..aadad3e 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -14,11 +14,20 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; */ const sidebars: SidebarsConfig = { agmlSidebar: [ - 'index', - 'development', - 'credits', - 'code-of-conduct', - 'license', + { + type: 'category', + label: 'Guide', + collapsible: false, + className: 'sidebarCategoryLabel', + items: ['index', 'development'], + }, + { + type: 'category', + label: 'Project', + collapsible: false, + className: 'sidebarCategoryLabel', + items: ['credits', 'code-of-conduct', 'license'], + }, ], }; diff --git a/src/components/DatasetMetadataModal.module.css b/src/components/DatasetMetadataModal.module.css index d6d7292..e4686c1 100644 --- a/src/components/DatasetMetadataModal.module.css +++ b/src/components/DatasetMetadataModal.module.css @@ -10,15 +10,15 @@ } .modal { - width: min(1200px, 92vw); + width: min(1140px, 95vw); max-height: min(88vh, 920px); overflow: auto; - border-radius: 28px; - border: 1px solid var(--agml-border); - background: var(--agml-surface); + border-radius: 12px; + border: 1px solid var(--agml-border-strong); + background: var(--agml-surface-strong); box-shadow: var(--agml-shadow-strong); color: var(--agml-text); - padding: 1.4rem 1.4rem 1.2rem; + padding: 2rem; } .header { @@ -26,22 +26,15 @@ align-items: flex-start; justify-content: space-between; gap: 1rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--agml-border); -} - -.kicker { - margin: 0 0 0.35rem; - font-size: 0.72rem; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--agml-muted); + margin-bottom: 1.1rem; } .title { - margin: 0 0 0.6rem; - font-size: clamp(1.4rem, 2vw, 2rem); - line-height: 1.15; + font-family: var(--ifm-code-font-family); + font-weight: 700; + font-size: 1.25rem; + margin: 0 0 0.5rem; + line-height: 1.2; overflow-wrap: anywhere; } @@ -53,8 +46,8 @@ .taskBadge { font-family: var(--ifm-code-font-family); - font-size: 0.72rem; - padding: 0.2rem 0.55rem; + font-size: 0.65rem; + padding: 0.15rem 0.5rem; border-radius: 4px; display: inline-block; white-space: nowrap; @@ -82,7 +75,7 @@ .tag { font-family: var(--ifm-code-font-family); - font-size: 0.72rem; + font-size: 0.68rem; padding: 0.2rem 0.55rem; border-radius: 4px; display: inline-block; @@ -93,79 +86,66 @@ .closeButton { flex: 0 0 auto; - width: 34px; - height: 34px; + width: 30px; + height: 30px; display: flex; align-items: center; justify-content: center; border: 1px solid var(--agml-border-strong); - border-radius: 8px; - background: var(--agml-surface-strong); + border-radius: 6px; + background: var(--agml-surface-soft); color: var(--agml-text); padding: 0; - font-size: 17px; + font-size: 15px; line-height: 1; cursor: pointer; } .closeButton:hover, .closeButton:focus-visible { - border-color: var(--ifm-color-primary); + background: var(--agml-border); outline: none; } .detailGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); - gap: 0.9rem; - margin: 1rem 0 0; + gap: 0.85rem; + margin: 1.25rem 0 1.75rem; } .detailItem { - padding: 0.85rem 0.9rem; - border-radius: 18px; - border: 1px solid var(--agml-border); - background: var(--agml-surface-strong); + min-width: 0; } .detailLabel { - margin: 0 0 0.35rem; - font-size: 0.68rem; - font-weight: 600; + margin: 0 0 0.3rem; + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; + letter-spacing: 0.05em; text-transform: uppercase; - letter-spacing: 0.14em; color: var(--agml-muted); } .detailValue { margin: 0; - font-size: 0.95rem; + font-size: 0.82rem; line-height: 1.4; color: var(--agml-text); - font-weight: 600; overflow-wrap: anywhere; } -.secondaryGrid { - display: grid; - grid-template-columns: 1.2fr 0.8fr; - gap: 1rem; - margin-top: 1rem; -} - -.secondarySection { - padding: 1rem; - border-radius: 20px; - border: 1px solid var(--agml-border); - background: var(--agml-surface-strong); +.section { + margin-bottom: 1.75rem; } .sectionTitle { - margin: 0 0 0.65rem; - font-size: 0.82rem; + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.14em; + letter-spacing: 0.05em; color: var(--agml-muted); + margin: 0 0 0.65rem; } .bodyText { @@ -192,7 +172,7 @@ .expandButton { font-family: var(--ifm-code-font-family); - font-size: 0.72rem; + font-size: 0.68rem; padding: 0.2rem 0.55rem; border-radius: 4px; background: transparent; @@ -208,12 +188,25 @@ outline: none; } +.figure { + margin: 0; +} + +.exampleImage { + display: block; + width: 100%; + max-height: 65vh; + object-fit: contain; + border-radius: 8px; + background: var(--agml-surface-soft); +} + .snippetRow { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 0.75rem; - background: var(--agml-surface-strong); + background: var(--agml-page-bg); border: 1px solid var(--agml-border); border-radius: 6px; padding: 0.6rem 0.85rem; @@ -222,17 +215,19 @@ .snippetCode { font-family: var(--ifm-code-font-family); - font-size: 0.82rem; + font-size: 0.78rem; + line-height: 1.6; color: var(--agml-text); overflow-x: auto; - white-space: nowrap; + white-space: pre; } .snippetCopyButton { flex-shrink: 0; + align-self: center; font-family: var(--ifm-code-font-family); - font-size: 0.72rem; - padding: 0.35rem 0.7rem; + font-size: 0.68rem; + padding: 0.3rem 0.65rem; border-radius: 5px; border: 1px solid var(--agml-border-strong); background: transparent; @@ -247,69 +242,33 @@ outline: none; } -.footer { - display: grid; - gap: 1rem; - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--agml-border); -} - -.figure { - margin: 0; -} - -.exampleImage { - display: block; - width: 100%; - height: 65vh; - object-fit: contain; - border-radius: 18px; - border: 1px solid var(--agml-border); - background: var(--agml-surface-strong); -} - .linkRow { display: flex; flex-wrap: wrap; gap: 0.6rem; + margin-bottom: 1.75rem; } -.externalLink { - display: inline-flex; - width: fit-content; - border-radius: 999px; - padding: 0.6rem 1rem; - border: 1px solid var(--ifm-color-primary); - background: var(--ifm-color-primary); - color: #fff; - text-decoration: none; - font-weight: 600; -} - -.externalLink:hover, -.externalLink:focus-visible { - background: var(--ifm-color-primary-dark); - border-color: var(--ifm-color-primary-dark); - outline: none; -} - +.externalLink, .hfLink { display: inline-flex; width: fit-content; - border-radius: 999px; - padding: 0.6rem 1rem; - border: 1px solid #f5a000; - background: #f5a000; - color: #fff; + border-radius: 6px; + padding: 0.5rem 1rem; + border: 1px solid var(--agml-border-strong); + background: var(--agml-tag-bg); + color: var(--agml-tag-fg); text-decoration: none; - font-weight: 600; + font-size: 0.82rem; + cursor: pointer; } +.externalLink:hover, +.externalLink:focus-visible, .hfLink:hover, .hfLink:focus-visible { - background: #d98c00; - border-color: #d98c00; + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); outline: none; } @@ -322,123 +281,167 @@ flex-wrap: wrap; align-items: flex-end; gap: 1rem; - margin: 0.75rem 0 1rem; - padding: 0.9rem 1rem; - border-radius: 16px; + margin-bottom: 0.9rem; + padding: 0.75rem; + border-radius: 8px; border: 1px solid var(--agml-border); - background: var(--agml-surface); + background: var(--agml-surface-soft); } -.filterRange { +.filterGroup { display: flex; flex-direction: column; gap: 0.4rem; - min-width: 160px; } -.filterRangeLabel { - font-size: 0.7rem; +.filterGroupLabel { + margin: 0; + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; text-transform: uppercase; - letter-spacing: 0.14em; + letter-spacing: 0.04em; color: var(--agml-muted); } -.filterRangeInputs { +.filterGroupOptions { display: flex; - align-items: center; - gap: 0.4rem; - color: var(--agml-muted); + flex-wrap: wrap; + gap: 0.65rem; } -.filterRangeInput { - width: 100%; - min-width: 0; - border-radius: 10px; - border: 1px solid var(--agml-border-strong); - padding: 0.5rem 0.6rem; - background: var(--agml-surface-strong); +.filterCheckboxRow { + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.75rem; + cursor: pointer; color: var(--agml-text); - font-size: 0.88rem; + white-space: nowrap; } -.clearFiltersButton { - border: 1px solid var(--agml-border-strong); - background: transparent; - border-radius: 999px; - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: var(--agml-text); +.checkbox { + appearance: none; + -webkit-appearance: none; + width: 15px; + height: 15px; + flex-shrink: 0; + border-radius: 4px; + border: 1.5px solid var(--agml-border-strong); + background: var(--agml-page-bg); + position: relative; cursor: pointer; - white-space: nowrap; + transition: background 0.12s, border-color 0.12s; } -.leaderboardTable { - width: 100%; - max-width: 100%; - table-layout: fixed; - margin-top: 0.6rem; - border-collapse: collapse; - font-size: 0.88rem; +.checkbox:hover { + border-color: var(--agml-muted); } -.leaderboardTable thead, -.leaderboardTable tbody tr { - background: transparent; +.checkbox:checked { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); } -.leaderboardTable thead tr { - border-bottom: none; +.checkbox::after { + content: ''; + position: absolute; + left: 3.5px; + top: 0.5px; + width: 4px; + height: 7px; + border-style: solid; + border-width: 0 2px 2px 0; + border-color: var(--agml-page-bg); + transform: rotate(45deg); + opacity: 0; } -.leaderboardTable tr:nth-child(2n) { - background-color: transparent; +.checkbox:checked::after { + opacity: 1; } -.leaderboardTable th, -.leaderboardTable td { - padding: 0.55rem 0.7rem; - text-align: left; - border: none; - border-bottom: 1px solid var(--agml-border); - white-space: normal; - overflow-wrap: anywhere; +.filterRange { + display: flex; + flex-direction: column; + gap: 0.4rem; } -.colRank { - width: 6%; +.filterRangeLabel { + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--agml-muted); } -.colModel { - width: 16%; +.filterRangeInput { + width: 96px; + border-radius: 5px; + border: 1px solid var(--agml-border-strong); + padding: 0.4rem 0.55rem; + background: var(--agml-surface-strong); + color: var(--agml-text); + font-family: var(--ifm-code-font-family); + font-size: 0.75rem; } -.colResultType { - width: 12%; +.clearFiltersButton { + border: 1px solid var(--agml-border-strong); + background: transparent; + border-radius: 999px; + padding: 0.45rem 0.9rem; + font-size: 0.78rem; + color: var(--agml-text); + cursor: pointer; + white-space: nowrap; } -.colSplit { - width: 13%; +.tableWrap { + border: 1px solid var(--agml-border); + border-radius: 8px; + overflow-x: auto; } -.colConfig { - width: 12%; +.leaderboardTable { + width: 100%; + min-width: 960px; + font-size: 0.78rem; } -.colMetric { - width: 22%; +.tableRow { + display: grid; + grid-template-columns: 42px minmax(150px, 1fr) 120px 70px 125px 130px 120px 65px; + gap: 0.5rem; + align-items: start; } -.colTime { - width: 15%; +.tableRow[role='row']:first-child { + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--agml-muted); + background: var(--agml-tag-bg); + padding: 0.5rem 0.75rem; + align-items: center; } -.colPlatform { - width: 8%; +.tableRow:not(:first-child) { + padding: 0.55rem 0.75rem; + border-top: 1px solid var(--agml-border); } +.tableRow > [role='columnheader'] { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} -.notesCell { - background: var(--agml-surface); +.simpleCell { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; } .rankCell { @@ -450,6 +453,7 @@ background: none; border: none; padding: 0; + max-width: 100%; font-family: var(--ifm-code-font-family); font-weight: 500; color: var(--agml-text); @@ -458,6 +462,23 @@ text-underline-offset: 3px; cursor: pointer; text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + +.scoresCell { + display: flex; + flex-direction: column; + gap: 0.15rem; + font-family: var(--ifm-code-font-family); + font-size: 0.72rem; + color: var(--ifm-color-primary); +} + +.scoresCell span { + white-space: nowrap; } .stackCell { @@ -465,30 +486,36 @@ flex-direction: column; gap: 0.15rem; font-family: var(--ifm-code-font-family); - font-size: 0.8rem; + font-size: 0.72rem; + color: var(--agml-muted); } -.notesText { - margin: 0; - color: var(--agml-text); +.stackCell span { + white-space: nowrap; +} + +.notesRow { + padding: 0.5rem 0.75rem 0.75rem 2.6rem; + background: var(--agml-surface-soft); + border-top: 1px dashed var(--agml-border); + font-size: 0.75rem; line-height: 1.5; - overflow-wrap: anywhere; } -.leaderboardTable th { - font-size: 0.68rem; - font-weight: 600; +.notesLabel { + margin: 0 0 0.2rem; + font-family: var(--ifm-code-font-family); + font-size: 0.62rem; text-transform: uppercase; - letter-spacing: 0.1em; + letter-spacing: 0.05em; color: var(--agml-muted); } -.leaderboardTable td { +.notesText { + margin: 0; color: var(--agml-text); -} - -.leaderboardTable tbody tr:last-child td { - border-bottom: none; + line-height: 1.5; + overflow-wrap: anywhere; } .leaderboardTable a { @@ -504,15 +531,15 @@ @media (max-width: 720px) { .modal { - padding: 1.1rem; - border-radius: 22px; + padding: 1.25rem; + border-radius: 10px; } .header { flex-direction: column; } - .secondaryGrid { - grid-template-columns: 1fr; + .detailGrid { + grid-template-columns: repeat(2, 1fr) !important; } } diff --git a/src/components/DatasetMetadataModal.tsx b/src/components/DatasetMetadataModal.tsx index c001073..e7bebe8 100644 --- a/src/components/DatasetMetadataModal.tsx +++ b/src/components/DatasetMetadataModal.tsx @@ -3,7 +3,6 @@ import type { Dataset } from '../lib/datasets'; import { formatDisplayLocation } from '../lib/datasets'; import { METRIC_CATEGORY_LABELS, useDatasetPerformance } from '../lib/performance'; import type { MetricCategory, PerformanceEntry } from '../lib/performance'; -import { MultiSelectDropdown } from './MultiSelectDropdown'; import styles from './DatasetMetadataModal.module.css'; function formatImageCount(count: number | null) { @@ -95,18 +94,49 @@ function formatLoaderInstructions(dataset: Dataset) { if (dataset.source === 'huggingface') { return { title: 'Load from Hugging Face', - body: `Use agml.data.hf_loader.HuggingFaceDataLoader("Project-AgML/${dataset.name}") to load this dataset from Hugging Face.`, - code: `from agml.data.hf_loader import HuggingFaceDataLoader; loader = HuggingFaceDataLoader("Project-AgML/${dataset.name}")`, + code: `from agml.data.hf_loader import HuggingFaceDataLoader\nloader = HuggingFaceDataLoader("Project-AgML/${dataset.name}")`, }; } return { title: 'Load with AgML', - body: `Use agml.data.AgMLDataLoader("${dataset.name}") to load this dataset locally through AgML.`, - code: `import agml; loader = agml.data.AgMLDataLoader("${dataset.name}")`, + code: `import agml\nloader = agml.data.AgMLDataLoader("${dataset.name}")`, }; } +function InlineFilterGroup({ + label, + options, + selected, + onToggle, + formatOption = (value: string) => value, +}: { + label: string; + options: string[]; + selected: string[]; + onToggle: (value: string) => void; + formatOption?: (value: string) => string; +}) { + return ( +
+

{label}

+
+ {options.map((option) => ( + + ))} +
+
+ ); +} + export function DatasetMetadataModal({ dataset, open, @@ -218,12 +248,10 @@ export function DatasetMetadataModal({ if (!open || dataset == null) return null; - const detailRows = [ + const metadataRows = [ ['Location', formatDisplayLocation(dataset.location)], ['Sensor modality', formatValue(dataset.sensor_modality)], ['Platform', formatValue(dataset.platform)], - ['Input format', formatValue(dataset.input_data_format)], - ['Annotation format', formatValue(dataset.annotation_format)], ['Number of images', formatImageCount(dataset.num_images)], ['Size', formatBytesDecimal(dataset.zip_size_bytes)], ...(dataset.augmented_num_images != null @@ -248,7 +276,6 @@ export function DatasetMetadataModal({ >
-

Dataset metadata

{dataset.name}

@@ -267,8 +294,8 @@ export function DatasetMetadataModal({
-
- {detailRows.map(([label, value]) => ( +
+ {metadataRows.map(([label, value]) => (
{label}
{value}
@@ -276,262 +303,235 @@ export function DatasetMetadataModal({ ))}
- {(cropList.length > 0 || classList.length > 0 || dataset.stats_mean || dataset.stats_std) && ( -
- {cropList.length > 0 && ( -
-

Crops

-
- {(cropsExpanded ? cropList : cropList.slice(0, 8)).map((crop) => ( - {crop} - ))} - {!cropsExpanded && cropList.length > 8 && ( - - )} -
-
- )} - {classList.length > 0 && ( -
-

Classes

-
- {(classesExpanded ? classList : classList.slice(0, 8)).map((cls) => ( - {cls} - ))} - {!classesExpanded && classList.length > 8 && ( - - )} -
-
- )} - {(dataset.stats_mean || dataset.stats_std) && ( -
-

Stats

-

- Mean: {formatArray(dataset.stats_mean)} -

-

- Std: {formatArray(dataset.stats_std)} -

-
- )} -
+ {cropList.length > 0 && ( +
+

Crops

+
+ {(cropsExpanded ? cropList : cropList.slice(0, 10)).map((crop) => ( + {crop} + ))} + {!cropsExpanded && cropList.length > 10 && ( + + )} +
+
)} -
-
-

Sample image

- {hasExampleImage(dataset.examples_image_url) ? ( -
- {`Example -
- ) : ( -

No example image is available for this dataset.

- )} + {classList.length > 0 && ( +
+

Classes

+
+ {(classesExpanded ? classList : classList.slice(0, 10)).map((cls) => ( + {cls} + ))} + {!classesExpanded && classList.length > 10 && ( + + )} +
+ )} -
-

{loader.title}

-

{loader.body}

-
- {loader.code} - -
+ {(dataset.stats_mean || dataset.stats_std) && ( +
+

Stats

+

+ Mean: {formatArray(dataset.stats_mean)} +

+

+ Std: {formatArray(dataset.stats_std)} +

+ )} - {(dataset.documentation || dataset.hf_link) && ( -
- {dataset.documentation && ( - - Open source documentation - - )} - {dataset.hf_link && ( - - View on Hugging Face - - )} -
+
+

Sample image

+ {hasExampleImage(dataset.examples_image_url) ? ( +
+ {`Example +
+ ) : ( +

No example image is available for this dataset.

)} +
+ +
+

{loader.title}

+
+ {loader.code} + +
+
+ + {(dataset.documentation || dataset.hf_link) && ( +
+ {dataset.documentation && ( + + Open source documentation + + )} + {dataset.hf_link && ( + + View on Hugging Face + + )} +
+ )} -
-

Model performance leaderboard

- {datasetPerformance.loading ? ( -

Loading leaderboard…

- ) : datasetPerformance.data && datasetPerformance.data.entries.length > 0 ? ( -
- {datasetPerformance.data.metric && ( -

- Metric: {datasetPerformance.data.metric} -

- )} - -
- toggleFilter(setMetricTypeFilter, value)} - formatOption={(value) => METRIC_CATEGORY_LABELS[value as MetricCategory]} - /> - toggleFilter(setTunedFilter, value)} - formatOption={formatTunedKey} +
+

+ Leaderboard{datasetPerformance.data?.metric ? ` — ${datasetPerformance.data.metric}` : ''} +

+ {datasetPerformance.loading ? ( +

Loading leaderboard…

+ ) : datasetPerformance.data && datasetPerformance.data.entries.length > 0 ? ( +
+
+ toggleFilter(setMetricTypeFilter, value)} + formatOption={(value) => METRIC_CATEGORY_LABELS[value as MetricCategory]} + /> + toggleFilter(setTunedFilter, value)} + formatOption={formatTunedKey} + /> + toggleFilter(setOptimizedFilter, value)} + formatOption={formatOptimizedKey} + /> + toggleFilter(setPlatformFilter, value)} + /> +
+ + setTrainingTimeMax(event.target.value)} + className={styles.filterRangeInput} /> - toggleFilter(setOptimizedFilter, value)} - formatOption={formatOptimizedKey} - /> - toggleFilter(setPlatformFilter, value)} +
+
+ + setTrainingSizeMin(event.target.value)} + className={styles.filterRangeInput} /> -
- -
- setTrainingTimeMax(event.target.value)} - className={styles.filterRangeInput} - /> -
-
-
- -
- setTrainingSizeMin(event.target.value)} - className={styles.filterRangeInput} - /> -
-
- {hasActiveLeaderboardFilters && ( - - )}
+ {hasActiveLeaderboardFilters && ( + + )} +
- {filteredEntries.length === 0 ? ( -

No results match the selected filters.

- ) : ( - - - - - - - - - - - - - - - - - - - - - - - - - {filteredEntries.map((entry, index) => { - const rowKey = `${entry.model}-${entry.variant ?? 'default'}-${index}`; - const isExpanded = expandedRowKeys.has(rowKey); - return ( - - - - - - - - - - - - {isExpanded && ( - - - - )} - - ); - })} - -
RankModelResultSplit %DataScoresNorm s/imgPlat.
#{entry.rank ?? index + 1} - - {formatResultType(entry)}{entry.splitBreakdown ?? '—'}{entry.datasetConfig ?? '—'} - {entry.metrics.length > 0 ? ( -
- {entry.metrics.map((metric) => ( - {metric.label}: {metric.value.toFixed(3)} - ))} -
+ {filteredEntries.length === 0 ? ( +

No results match the selected filters.

+ ) : ( +
+
+
+ Rank + Model + Result + Split% + Config + Scores + Norm s/img + Plat. +
+ {filteredEntries.map((entry, index) => { + const rowKey = `${entry.model}-${entry.variant ?? 'default'}-${index}`; + const isExpanded = expandedRowKeys.has(rowKey); + return ( + +
+ #{entry.rank ?? index + 1} + +
-
- train {formatTimePerImage(entry.trainTimePerImage)} - inf {formatTimePerImage(entry.infTimePerImage)} + entry.model + )}{' '} + {isExpanded ? '▾' : '▸'} + + + {formatResultType(entry)} + {entry.splitBreakdown ?? '—'} + {entry.datasetConfig ?? '—'} + + {entry.metrics.length > 0 ? ( +
+ {entry.metrics.map((metric) => ( + {metric.label}: {metric.value.toFixed(3)} + ))}
-
{entry.platform ?? '—'}
-

Necessary notes

-

{entry.notes ?? 'No additional notes for this result.'}

-
- )} -
- ) : ( -

No leaderboard results have been submitted for this dataset yet.

- )} -
-
+ ) : ( + {formatMetricScore(entry)} + )} + + +
+ train {formatTimePerImage(entry.trainTimePerImage)} + inf {formatTimePerImage(entry.infTimePerImage)} +
+
+ {entry.platform ?? '—'} +
+ {isExpanded && ( +
+

Notes

+

{entry.notes ?? 'No additional notes for this result.'}

+
+ )} + + ); + })} +
+
+ )} + + ) : ( +

No leaderboard results have been submitted for this dataset yet.

+ )} + ); diff --git a/src/components/LeaderboardDetailModal.module.css b/src/components/LeaderboardDetailModal.module.css index 77e3531..268e618 100644 --- a/src/components/LeaderboardDetailModal.module.css +++ b/src/components/LeaderboardDetailModal.module.css @@ -10,7 +10,7 @@ } .modal { - width: min(880px, 92vw); + width: min(1040px, 95vw); max-height: min(88vh, 900px); overflow-y: auto; border-radius: 12px; @@ -18,7 +18,7 @@ background: var(--agml-surface-strong); box-shadow: var(--agml-shadow-strong); color: var(--agml-text); - padding: 1.75rem; + padding: 2rem; } .header { @@ -71,8 +71,8 @@ font-size: 0.65rem; padding: 0.15rem 0.5rem; border-radius: 4px; - background: var(--agml-surface-soft); - color: var(--agml-muted); + background: var(--agml-tag-bg); + color: var(--agml-tag-fg); display: inline-block; } @@ -104,7 +104,7 @@ } .closeButton:hover { - background: var(--agml-accent-soft); + background: var(--agml-border); } .sectionTitle { @@ -125,43 +125,42 @@ .table { width: 100%; - border-collapse: collapse; - font-size: 0.82rem; -} - -.table thead, -.table tbody tr { - background: transparent; + font-size: 0.72rem; } -.table thead tr { - border-bottom: none; +.tableRow { + display: grid; + grid-template-columns: minmax(200px, 1.4fr) 70px 100px minmax(280px, 1.6fr); + gap: 0.75rem; + align-items: start; } -.table tr:nth-child(2n) { - background-color: transparent; +.tableRow[role='row']:first-child { + font-family: var(--ifm-code-font-family); + font-size: 0.6rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--agml-muted); + background: var(--agml-tag-bg); + padding: 0.5rem 0.75rem; + align-items: center; } -.table th, -.table td { - text-align: left; +.tableRow:not(:first-child) { padding: 0.55rem 0.75rem; - border: none; - border-bottom: 1px solid var(--agml-border); - vertical-align: top; + border-top: 1px solid var(--agml-border); } -.table th { - font-family: var(--ifm-code-font-family); - font-size: 0.62rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--agml-muted); - background: var(--agml-surface-soft); +.tableRow > [role='columnheader'] { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.table tbody tr:last-child > td { - border-bottom: none; +.simpleCell { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .clickableRow { @@ -183,6 +182,7 @@ display: flex; align-items: center; gap: 0.4rem; + max-width: 100%; background: none; border: none; padding: 0; @@ -191,6 +191,9 @@ color: var(--agml-text); text-decoration: underline dashed var(--agml-muted); text-decoration-thickness: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .expandChevron { @@ -200,11 +203,11 @@ .viewDatasetLink { font-family: var(--ifm-code-font-family); - font-size: 0.65rem; - padding: 0.15rem 0.5rem; + font-size: 0.625rem; + padding: 0.1rem 0.45rem; border-radius: 4px; - background: var(--agml-surface-soft); - color: var(--agml-muted); + background: var(--agml-tag-bg); + color: var(--agml-tag-fg); text-decoration: none; white-space: nowrap; } @@ -221,16 +224,22 @@ gap: 0.2rem; font-family: var(--ifm-code-font-family); color: var(--ifm-color-primary); - font-size: 0.75rem; + font-size: 0.66rem; +} + +.scoresCell span { + white-space: nowrap; } .scoresCell span span { color: var(--agml-muted); } -.notesRow td { +.notesRow { + padding: 0.5rem 0.75rem 0.75rem; background: var(--agml-surface-soft); border-top: 1px dashed var(--agml-border); - font-size: 0.78rem; + font-size: 0.72rem; + line-height: 1.5; color: var(--agml-muted); } diff --git a/src/components/LeaderboardDetailModal.tsx b/src/components/LeaderboardDetailModal.tsx index 05d1723..2a5c0f4 100644 --- a/src/components/LeaderboardDetailModal.tsx +++ b/src/components/LeaderboardDetailModal.tsx @@ -79,15 +79,15 @@ export function LeaderboardDetailModal({ >
+

+ {entry.model} +

{entry.machineLearningTask ? toLabel(entry.machineLearningTask) : 'Unknown task'} {entry.resultType}
-

- {entry.model} -

{entry.appearances} result{entry.appearances === 1 ? '' : 's'} across {entry.datasets.length} dataset {entry.datasets.length === 1 ? '' : 's'} @@ -100,83 +100,93 @@ export function LeaderboardDetailModal({

Datasets included ({entry.datasetDetails.length})

- - - - - - - - - - - {entry.datasetDetails.map((detail, index) => { - const rowKey = `${detail.dataset}-${index}`; - const isExpanded = expandedKeys.has(rowKey); - const scoreLines = ( - [ - ['F1', 'f1'], - ['mAP', 'map'], - ['Precision', 'precision'], - ['Recall', 'recall'], - ] as [string, 'f1' | 'map' | 'precision' | 'recall'][] - ) - .map(([label, category]) => { - const score = detail.scores[category]; - const pctl = detail.percentiles[category]; - if (score == null || pctl == null) return null; - return [label, formatScore(score), formatPercentile(pctl)] as [string, string, string]; - }) - .filter((line): line is [string, string, string] => line != null); +
+
+ Dataset + Split % + Config + Scores (percentile) +
+ {entry.datasetDetails.map((detail, index) => { + const rowKey = `${detail.dataset}-${index}`; + const isExpanded = expandedKeys.has(rowKey); + const scoreLines = ( + [ + ['F1', 'f1'], + ['mAP', 'map'], + ['Precision', 'precision'], + ['Recall', 'recall'], + ] as [string, 'f1' | 'map' | 'precision' | 'recall'][] + ) + .map(([label, category]) => { + const score = detail.scores[category]; + const pctl = detail.percentiles[category]; + if (score == null || pctl == null) return null; + return [label, formatScore(score), formatPercentile(pctl)] as [string, string, string]; + }) + .filter((line): line is [string, string, string] => line != null); - return ( - -
toggleExpanded(rowKey)} aria-expanded={isExpanded}> - - - - - - {isExpanded && ( - - - - )} - - ); - })} - -
DatasetSplit %ConfigScores (percentile)
-
- + event.stopPropagation()} + > + view dataset + +
+ + + {detail.splitBreakdown ?? '—'} + + + {detail.datasetConfig ?? '—'} + + + {scoreLines.length === 0 ? ( + '—' + ) : ( +
+ {scoreLines.map(([label, score, pctl]) => ( + + {label} {score} ({pctl}) - - event.stopPropagation()} - > - view dataset - + ))}
-
{detail.splitBreakdown ?? '—'}{detail.datasetConfig ?? '—'} - {scoreLines.length === 0 ? ( - '—' - ) : ( -
- {scoreLines.map(([label, score, pctl]) => ( - - {label} {score} ({pctl}) - - ))} -
- )} -
- {formatResultType(detail)} · {detail.platform ?? 'unknown platform'} -
+ )} + +
+ {isExpanded && ( +
+ {formatResultType(detail)} · {detail.platform ?? 'unknown platform'} +
+ )} + + ); + })} +
diff --git a/src/components/MultiSelectDropdown.module.css b/src/components/MultiSelectDropdown.module.css index e0212dc..71d0a5f 100644 --- a/src/components/MultiSelectDropdown.module.css +++ b/src/components/MultiSelectDropdown.module.css @@ -22,12 +22,14 @@ display: flex; justify-content: space-between; align-items: center; - border-radius: 14px; + border-radius: 6px; border: 1px solid var(--agml-border-strong); padding: 0.6rem 0.9rem; background: var(--agml-surface-strong); - font-size: 0.92rem; + font-family: var(--ifm-code-font-family); + font-size: 0.82rem; color: var(--agml-text); + width: 100%; } .dropdownChevron { @@ -39,13 +41,13 @@ top: calc(100% + 0.4rem); left: 0; right: 0; - border-radius: 16px; - border: 1px solid var(--agml-border); + border-radius: 6px; + border: 1px solid var(--agml-border-strong); background: var(--agml-surface-strong); - padding: 0.4rem 0; + padding: 0.35rem; max-height: 240px; overflow: auto; - box-shadow: var(--agml-shadow); + box-shadow: var(--agml-shadow-strong); z-index: 40; } @@ -59,32 +61,42 @@ display: flex; align-items: center; gap: 0.5rem; - padding: 0.55rem 0.9rem; + padding: 0.4rem 0.5rem; width: 100%; text-align: left; border: none; + border-radius: 4px; background: transparent; cursor: pointer; - font-size: 0.9rem; + font-size: 0.82rem; + color: var(--agml-text); +} + +.dropdownOption:hover { + background: var(--agml-tag-bg); } .dropdownOptionSelected { - background: var(--agml-accent-soft); - color: var(--ifm-color-primary); + color: var(--agml-text); } .dropdownCheckbox { - width: 1.2rem; - height: 1.2rem; - border-radius: 6px; - border: 1px solid var(--agml-border-strong); + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 4px; + border: 1.5px solid var(--agml-border-strong); + background: var(--agml-page-bg); display: inline-flex; align-items: center; justify-content: center; - font-size: 0.8rem; - color: var(--ifm-color-primary); + font-size: 0.65rem; + color: transparent; + transition: background 0.12s, border-color 0.12s; } -:global(html[data-theme='dark']) .dropdownOptionSelected { - color: var(--ifm-color-primary-lightest); +.dropdownOptionSelected .dropdownCheckbox { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: var(--agml-page-bg); } diff --git a/src/css/custom.css b/src/css/custom.css index 0619427..cf09f33 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -144,6 +144,51 @@ body { .navbar { border-bottom: 1px solid var(--agml-border); + --ifm-navbar-height: 64px; + --ifm-navbar-padding-horizontal: 24px; + --ifm-navbar-item-padding-horizontal: 0; + --ifm-navbar-item-padding-vertical: 0; +} + +.navbar__brand { + margin-right: 20px; +} + +.navbar__brand::before { + content: ''; + display: inline-block; + width: 10px; + height: 10px; + margin-right: 10px; + border-radius: 2px; + background: var(--ifm-color-primary); + flex-shrink: 0; +} + +.navbar__title { + font-family: var(--ifm-code-font-family); + font-weight: 600; + font-size: 15px; + letter-spacing: -0.2px; +} + +.navbar__item { + padding: 4px; +} + +.navbar__link { + font-family: var(--ifm-code-font-family); + font-size: 12.5px; + font-weight: 400; + padding: 8px 12px; + border-radius: 6px; + color: var(--agml-muted); +} + +.navbar__link:hover, +.navbar__link--active { + background: var(--agml-tag-bg); + color: var(--agml-text); } .footer { @@ -151,8 +196,37 @@ body { } .theme-doc-markdown { - border-radius: 24px; - padding: 2rem; + border-radius: 0; + padding: 0; + background: transparent; + border: none; + box-shadow: none; + max-width: 760px; +} + +.theme-doc-markdown.markdown h1 { + font-size: 1.875rem; + font-weight: 600; + letter-spacing: -0.01em; + margin-bottom: 0.5rem; +} + +.theme-doc-markdown.markdown h2 { + font-size: 1.1875rem; + font-weight: 600; + scroll-margin-top: 1.25rem; +} + +.theme-doc-markdown.markdown h3 { + font-size: 1rem; + font-weight: 600; + scroll-margin-top: 1.25rem; +} + +.theme-doc-markdown.markdown p, +.theme-doc-markdown.markdown li { + font-size: 0.906rem; + line-height: 1.65; } .main-wrapper { @@ -162,22 +236,60 @@ body { .theme-doc-sidebar-container { border-right: 1px solid var(--agml-border); + background: var(--agml-surface-soft); + box-shadow: none; +} + +.theme-doc-sidebar-menu { + --ifm-menu-color: var(--agml-muted); + --ifm-menu-color-active: var(--agml-text); + --ifm-menu-color-background-hover: var(--agml-tag-bg); + --ifm-menu-color-background-active: var(--agml-tag-bg); + --ifm-menu-link-padding-vertical: 0.5rem; + --ifm-menu-link-padding-horizontal: 0.5rem; + padding: 1.5rem 1rem; +} + +.menu__link { + font-size: 0.82rem; + font-weight: 400; +} + +.sidebarCategoryLabel > .menu__list-item-collapsible { + background: transparent !important; +} + +.sidebarCategoryLabel > .menu__list-item-collapsible > .menu__link { + font-family: var(--ifm-code-font-family); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--agml-muted); + padding: 0.4rem 0.5rem; + pointer-events: none; +} + +.sidebarCategoryLabel + .sidebarCategoryLabel { + margin-top: 1.25rem; } .theme-doc-sidebar-item-link, .table-of-contents__link, -.menu__link, -.navbar__link, .footer__link-item { color: var(--agml-text); } -.theme-doc-sidebar-item-link--active, -.menu__link--active, -.navbar__link--active { +.theme-doc-sidebar-item-link--active { color: var(--ifm-color-primary); } +.theme-code-block { + border: 1px solid var(--agml-border); + border-radius: 8px; + box-shadow: none; +} + .theme-code-block pre, .prism-code { background: var(--agml-surface-strong) !important; diff --git a/src/lib/performance.ts b/src/lib/performance.ts index de4bbe4..856042b 100644 --- a/src/lib/performance.ts +++ b/src/lib/performance.ts @@ -85,7 +85,7 @@ function normalizeEntry(raw: unknown): PerformanceEntry | null { optimized: Boolean(raw.optimized), splitBreakdown: toText(raw.split_breakdown), trainPercentage: toNumber(raw.train_percentage), - datasetConfig: toText(raw.dataset_config ?? raw.config), + datasetConfig: toText(raw.dataset_config ?? raw.config) ?? 'raw', trainTimePerImage: toNumber(raw.train_time_per_image), infTimePerImage: toNumber(raw.inference_time_per_image), platform: toText(raw.platform ?? raw.device), @@ -236,6 +236,13 @@ function buildSplitBreakdown(entry: Record, finetune: unknown): return `${toShare(trainSamples)}/${toShare(testSamples)}/${toShare(valSamples)}`; } +// The result set encodes optimized as the string "yes" / "no" (not a boolean) — a finetune +// object's own optimized field, if present, may already be boolean, so both forms are accepted. +function isOptimized(value: unknown): boolean { + if (typeof value === 'string') return value.trim().toLowerCase() === 'yes'; + return Boolean(value); +} + function makeLeaderboardRow( entry: Record, metricKey: string, @@ -255,10 +262,13 @@ function makeLeaderboardRow( submitted_by: null, link: null, notes: variant === 'fine-tuned' ? buildFinetuneNote(finetune) : buildRunNote(entry), - optimized: Boolean(entry.optimized) || (isRecord(finetune) && Boolean(finetune.optimized)), + optimized: isOptimized(entry.optimized) || (isRecord(finetune) && isOptimized(finetune.optimized)), splitBreakdown: buildSplitBreakdown(entry, finetune), trainPercentage: computeTrainPercentage(entry, finetune), - datasetConfig: toText(entry.dataset_config) ?? toText(entry.split), + // dataset_config is the result set's own config field (raw / augmented) — no fallback to + // `split`, which is a different concept (the run's data split, e.g. "train"). Absent config + // means the run used the dataset's raw (unaugmented) form. + datasetConfig: toText(entry.dataset_config) ?? 'raw', trainTimePerImage: trainingTimeSeconds != null && trainSamples ? trainingTimeSeconds / trainSamples : null, infTimePerImage: isFiniteNumber(entry.inference_time_seconds) && isFiniteNumber(entry.num_samples) && entry.num_samples > 0 ? entry.inference_time_seconds / entry.num_samples diff --git a/src/pages/leaderboard/index.module.css b/src/pages/leaderboard/index.module.css index 90516c9..5b9a9b2 100644 --- a/src/pages/leaderboard/index.module.css +++ b/src/pages/leaderboard/index.module.css @@ -103,10 +103,44 @@ } .checkbox { - width: 15px; - height: 15px; - accent-color: var(--ifm-color-primary); + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 4px; + border: 1.5px solid var(--agml-border-strong); + background: var(--agml-page-bg); + position: relative; cursor: pointer; + transition: background 0.12s, border-color 0.12s; +} + +.checkbox:hover { + border-color: var(--agml-muted); +} + +.checkbox:checked { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); +} + +.checkbox::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border-style: solid; + border-width: 0 2px 2px 0; + border-color: var(--agml-page-bg); + transform: rotate(45deg); + opacity: 0; +} + +.checkbox:checked::after { + opacity: 1; } .results { @@ -128,45 +162,46 @@ .leaderboardTable { width: 100%; min-width: 1080px; - border-collapse: collapse; font-size: 0.85rem; } -.leaderboardTable thead, -.leaderboardTable tbody tr { - background: transparent; -} - -.leaderboardTable thead tr { - border-bottom: none; -} - -.leaderboardTable tr:nth-child(2n) { - background-color: transparent; -} - -.leaderboardTable th, -.leaderboardTable td { - text-align: left; - padding: 0.65rem 0.85rem; - border: none; - border-bottom: 1px solid var(--agml-border); +.tableRow { + display: grid; + grid-template-columns: + minmax(110px, 0.9fr) minmax(200px, 2fr) minmax(120px, 1.1fr) minmax(120px, 1.1fr) + minmax(120px, 1.1fr) minmax(120px, 1.1fr) minmax(140px, 1.3fr) minmax(80px, 0.7fr); + gap: 0.6rem; + align-items: center; } -.leaderboardTable th { +.tableRow[role='row']:first-child { font-family: var(--ifm-code-font-family); font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--agml-muted); - background: var(--agml-surface-soft); + background: var(--agml-tag-bg); + padding: 0.5rem 0.85rem; } -.leaderboardTable tbody tr:last-child td { - border-bottom: none; +.tableRow:not(:first-child) { + padding: 0.65rem 0.85rem; + border-top: 1px solid var(--agml-border); +} + +.tableRow [role='cell'], +.tableRow [role='columnheader'] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .sortHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.3rem; + width: 100%; background: none; border: none; padding: 0; @@ -176,6 +211,7 @@ text-transform: inherit; letter-spacing: inherit; color: inherit; + text-align: left; } .sortHeaderActive { diff --git a/src/pages/leaderboard/index.tsx b/src/pages/leaderboard/index.tsx index 29762e5..54e038b 100644 --- a/src/pages/leaderboard/index.tsx +++ b/src/pages/leaderboard/index.tsx @@ -233,67 +233,79 @@ export default function GlobalLeaderboardPage() { {!loading && !error && sorted.length > 0 && ( <>
- - - - - - - - - - - - - - - {pagedLeaderboard.map((entry) => { - const key = `${entry.model}|||${entry.machineLearningTask ?? ''}`; - return ( - setSelectedKey(key)}> - - - - - - - - - - ); - })} - -
CV TaskModel - - - - - - - - Result type# Results
- - {entry.machineLearningTask ? toLabel(entry.machineLearningTask) : 'Unknown'} - - - {entry.model} - - - - - - - - - {entry.resultType}{entry.appearances}
+
+
+ CV Task + Model + + + + + + + + + + + + + Result type + # Results +
+ {pagedLeaderboard.map((entry) => { + const key = `${entry.model}|||${entry.machineLearningTask ?? ''}`; + return ( +
setSelectedKey(key)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setSelectedKey(key); + } + }} + > + + + {entry.machineLearningTask ? toLabel(entry.machineLearningTask) : 'Unknown'} + + + + {entry.model} + + + + + + + + + + + + + + {entry.resultType} + {entry.appearances} +
+ ); + })} +
{pageCount > 1 && (
diff --git a/static/data/hf_datasets.json b/static/data/hf_datasets.json index 8de94cc..b80ae1b 100644 --- a/static/data/hf_datasets.json +++ b/static/data/hf_datasets.json @@ -2377,4 +2377,4 @@ "hf_link": "https://huggingface.co/datasets/Project-AgML/sorghum_weed_segmentation", "examples_image_url": "/img/agml/sample_images/sorghum_weed_segmentation_sample.webp" } -] +] \ No newline at end of file diff --git a/static/data/performance/global.json b/static/data/performance/global.json index e40976d..6cd9224 100644 --- a/static/data/performance/global.json +++ b/static/data/performance/global.json @@ -19,10 +19,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "openai/clip-vit-base-patch32", @@ -44,10 +44,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "kakaobrain/align-base", @@ -69,10 +69,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "google/siglip-base-patch16-224", @@ -94,10 +94,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "openai/clip-vit-base-patch32", @@ -119,10 +119,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "kakaobrain/align-base", @@ -144,10 +144,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "google/siglip-base-patch16-224", @@ -169,10 +169,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "openai/clip-vit-base-patch32", @@ -194,10 +194,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "kakaobrain/align-base", @@ -219,10 +219,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "google/siglip-base-patch16-224", @@ -244,10 +244,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "openai/clip-vit-base-patch32", @@ -269,10 +269,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "kakaobrain/align-base", @@ -294,10 +294,10 @@ ], "machine_learning_task": "image_classification", "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "kakaobrain/align-base", @@ -317,10 +317,10 @@ "crop_types": null, "machine_learning_task": null, "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "google/siglip-base-patch16-224", @@ -340,10 +340,10 @@ "crop_types": null, "machine_learning_task": null, "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" }, { "model": "openai/clip-vit-base-patch32", @@ -363,9 +363,9 @@ "crop_types": null, "machine_learning_task": null, "variant": "zero-shot", - "optimized": true, + "optimized": false, "platform": "cuda", "splitBreakdown": "-/100/-", - "datasetConfig": "train" + "datasetConfig": "raw" } ] \ No newline at end of file From a8108de4e70df9bc07c0ebf25dc0a66b7e82a2a8 Mon Sep 17 00:00:00 2001 From: Jared Smith Date: Wed, 22 Jul 2026 10:06:41 -0700 Subject: [PATCH 6/8] somre more ui updates and docs updates --- docs/development.mdx | 318 +++++++++--------- docs/index.mdx | 134 ++------ docusaurus.config.ts | 52 +-- src/components/MultiSelectDropdown.module.css | 4 + src/components/MultiSelectDropdown.tsx | 2 +- src/css/custom.css | 12 +- src/lib/datasets.ts | 6 +- src/pages/datasets/index.tsx | 18 +- src/pages/index.module.css | 46 ++- src/pages/index.tsx | 73 ++-- src/pages/leaderboard/index.module.css | 6 +- src/pages/leaderboard/index.tsx | 12 +- static/data/hf_datasets.json | 6 +- static/data/performance/global.json | 18 +- 14 files changed, 356 insertions(+), 351 deletions(-) diff --git a/docs/development.mdx b/docs/development.mdx index 1e345d7..0f39d91 100644 --- a/docs/development.mdx +++ b/docs/development.mdx @@ -10,109 +10,85 @@ Thank you for choosing to contribute to AgML! ## Contributing Data If you've found (or already have) a new dataset and you want to contribute the dataset to AgML, -then the instructions below will help you format and the data to the AgML standard. +then the instructions below will help you format the data to the AgML standard and publish it +through the Hugging Face Hub, which is how all new datasets are distributed. ### Dataset Formats Currently, we have image classification, object detection, and semantic segmentation datasets available -in AgML. These sources are synthesized to standard annotation formats, namely the following: +in AgML. Every dataset is published as a [Hugging Face `datasets`](https://huggingface.co/docs/datasets) repository +under the `Project-AgML` organization, backed by Parquet, with a `dataset_info` config describing its columns and +splits — not raw folders of images uploaded as-is. The column layout depends on the task: -- **Image Classification**: Image-To-Label-Number -- **Object Detection**: [COCO JSON](https://cocodataset.org/#format-data) -- **Semantic Segmentation**: Dense Pixel-Wise +- **Image Classification**: an `image` column plus a `label` `ClassLabel` column. +- **Object Detection**: an `image` column plus an `objects` column holding COCO-style bounding boxes and category IDs. +- **Semantic Segmentation**: an `image` column plus a `mask` column (single-channel `L`-mode image). + +`HuggingFaceDataLoader` (in `agml/data/hf_loader.py`) relies on these exact column names to cast `image`/`mask` to +the Hugging Face `Image` feature and to detect image-typed `label` columns, so new datasets must follow this schema +for the loader to work. #### Image Classification -Image classification datasets are organized in the following directory tree: +Build a `datasets.Dataset` (or `DatasetDict`, if you have predefined splits) with two columns: -``` - - ├──