"""VANTAGE-Bench Leaderboard — Hugging Face Space entry point. Data and ranks are computed once at startup. Filters narrow the displayed subset only; they never recompute official scores or ranks. """ from __future__ import annotations import re from datetime import datetime from pathlib import Path import gradio as gr from util.config import ( LEADERBOARD_TABS, PARAM_BUCKETS, PILLARS, ) from util.data import ModelRecord, load_results_json from util.ranking import GLOBAL_RANKS, compute_global_ranks from util.render import ( build_all_tables, build_overall_html_table, datatypes_for_tab, headers_for_tab, make_radar_svg, ) from css import CSS # --------------------------------------------------------------------------- # Startup — one-shot, immutable after this point # --------------------------------------------------------------------------- REPO_ROOT = Path(__file__).resolve().parent DATA_PATH = REPO_ROOT / "data" / "results.json" ABOUT_PATH = REPO_ROOT / "assets" / "about.md" DATA = load_results_json(DATA_PATH) compute_global_ranks(DATA.models, LEADERBOARD_TABS) # populates GLOBAL_RANKS # "2026-05-29" → "May 29, 2026" — no zero-padded day, cross-platform. _dt = datetime.strptime(DATA.updated, "%Y-%m-%d") _UPDATED_DISPLAY = f"{_dt.strftime('%B')} {_dt.day}, {_dt.year}" ABOUT_MD = ABOUT_PATH.read_text(encoding="utf-8") # Initial tables — no filters, sorted by overall rank _INIT_TABLES = build_all_tables(DATA.models, GLOBAL_RANKS) _INIT_OVERALL_HTML = build_overall_html_table(DATA.models, GLOBAL_RANKS["overall"]) _INIT_STATUS = f'

Showing all {len(DATA.models)} models

' # --------------------------------------------------------------------------- # Static markup # --------------------------------------------------------------------------- HEADER_HTML = """

VANTAGE-Bench

Video ANalysis Tasks Across Generalized Environments

A multi-task benchmark for evaluating Vision-Language Models on real-world fixed-camera footage across Warehouse, Transportation, and Smart Spaces, spanning Spatial, Spatio-Temporal, Temporal, and Semantic understanding.

3 Domains · 8 Tasks · 35,027 Annotations · 3,346 Media
""" # --------------------------------------------------------------------------- # Filter helpers # --------------------------------------------------------------------------- # Ordered pillar keys matching the tab order _PILLAR_ORDER: list[str] = ["overall", "spatial", "st", "temporal", "semantic"] def _param_matches(m: ModelRecord, bucket: str) -> bool: """True if model belongs to the selected param bucket. Buckets (keys mirror util.config.PARAM_BUCKETS): "all" – every model. "lt10" – strictly < 10B. "10to40" – inclusive 10B ≤ v ≤ 40B. "gt40" – strictly > 40B. Models with an undisclosed param count (param_value is None) — e.g. closed/API models like Gemini — are visible ONLY under "All sizes" (Option A): they are excluded from every numeric bucket so a user selecting a specific size range does not get noisy "unknown" rows. """ if bucket == "all": return True v = m.param_value if v is None: return False # null → only visible under "all" if bucket == "lt10": return v < 10.0 if bucket == "10to40": return 10.0 <= v <= 40.0 if bucket == "gt40": return v > 40.0 return True def _filter_models( search: str, model_type: str, access: str, params_bucket: str, verified_only: bool = False, ) -> list[ModelRecord]: models: list[ModelRecord] = list(DATA.models) # Free-text search (name or org, case-insensitive) if search and search.strip(): q = search.strip().lower() models = [m for m in models if q in m.name.lower() or q in m.organization.lower()] # Submission Type — UI label "System / Pipeline" maps to the internal # "ensemble" enum value (kept for schema/data backwards compatibility). if model_type and model_type != "All": rt = {"Single": "single", "System / Pipeline": "ensemble"}.get(model_type) if rt: models = [m for m in models if m.result_type == rt] # Access (Open-weight / Proprietary) if access and access != "All": ac = {"Open-weight": "open", "Proprietary": "closed"}.get(access) if ac: models = [m for m in models if m.type == ac] # Param size bucket models = [m for m in models if _param_matches(m, params_bucket)] # Verified only if verified_only: models = [m for m in models if m.verified] return models # --------------------------------------------------------------------------- # Reactive callback # --------------------------------------------------------------------------- def on_filter_change( search: str, model_type: str, access: str, params_bucket: str, verified_only: bool, ) -> tuple: """Rebuild the Overall HTML table and four pillar DataFrames from filters.""" filtered = _filter_models(search, model_type, access, params_bucket, verified_only) tables = build_all_tables(filtered, GLOBAL_RANKS) overall_html = build_overall_html_table(filtered, GLOBAL_RANKS["overall"]) n = len(filtered) total = len(DATA.models) if n == 0: status = '

No models match — adjust the filters.

' elif n == total: status = f'

Showing all {total} models

' else: status = f'

Showing {n} of {total} models

' # _PILLAR_ORDER is ["overall", "spatial", "st", "temporal", "semantic"]; # Overall outputs HTML, the four pillars output DataFrames. table_outputs: tuple = (overall_html,) + tuple(tables[p] for p in _PILLAR_ORDER[1:]) return table_outputs + (status,) + ("",) * 5 + (gr.update(visible=False),) * 5 # --------------------------------------------------------------------------- # Side-panel helpers # --------------------------------------------------------------------------- _PANEL_TASK_ROWS: list[tuple[str, str]] = [ ("2D Object Localization", "2d_localization"), ("2D Referring Expressions", "2d_referring_expressions"), ("Pointing", "2d_spatial_pointing"), ("SOT", "single_object_tracking"), ("Temporal Loc.", "temporal_localization"), ("DVC", "dense_video_captioning"), ("Event Verification", "event_verification"), ("VQA", "video_qa"), ] def _make_panel_html(m: ModelRecord) -> str: """Build the HTML string for the model detail side panel.""" # Model card link (shown in header) card_link = ( f' Model card ↗' if m.model_url else "" ) # Model info section type_label = "System / Pipeline" if m.result_type == "ensemble" else "Single" access_label = "Open-weight" if m.type == "open" else "Proprietary" info_cells = ( f'Parameters{m.params}' f'Type{type_label}' f'Access{access_label}' f'Evaluated{m.date_evaluated}' ) # Header badges (verified / new only) top_badges: list[str] = [] if m.verified: top_badges.append('✓ verified') if m.is_new: top_badges.append('new') top_badges_html = ( f'
{" ".join(top_badges)}
' if top_badges else "" ) # Task scores grid (all 8 tasks) task_cells = "".join( f'{label}' f'{f"{m.scores[f]:.1f}" if (f := field) in m.scores else "—"}' for label, field in _PANEL_TASK_ROWS ) radar_svg = make_radar_svg(m) return ( f'
' f'
' f'
{m.name}{card_link}
' f'
{m.organization}
' f'{top_badges_html}' f'
' f'
' f'
' f'
{info_cells}
' f'
' f'
{radar_svg}
' f'
' f'
{task_cells}
' f'
' ) def _on_row_select(evt: gr.SelectData, df_val) -> tuple: """Handle a dataframe row-click; returns (panel_html, column_visible_update).""" try: row_idx = evt.index[0] model_cell = str(df_val.iloc[row_idx, 1]) # Strip bold markdown, extract name before " · " match = re.search(r'data-n="([^"]+)"', model_cell) name = match.group(1) if match else model_cell.split(' · ')[0].strip('*').strip() m = next((x for x in DATA.models if x.name == name), None) if m is None: return "", gr.update(visible=False) return _make_panel_html(m), gr.update(visible=True) except Exception: return "", gr.update(visible=False) def _on_overall_row_click_id(raw: str) -> tuple: """Handle a row click from the Overall HTML table. The JS bridge in _RESIZE_JS writes "{model_id}|{timestamp}" into the hidden gr.Textbox (#lb-overall-selected-id). The timestamp suffix forces a value change even when the same row is clicked twice (e.g. close panel → click same row again), so gr.Textbox.change always fires. """ if not raw: return "", gr.update(visible=False) model_id = raw.split("|", 1)[0] m = DATA.model_by_id.get(model_id) if m is None: return "", gr.update(visible=False) return _make_panel_html(m), gr.update(visible=True) # --------------------------------------------------------------------------- # UI layout # --------------------------------------------------------------------------- # Gradio tuple-choice lists so the component returns keys, not display labels _PARAMS_CHOICES: list[tuple[str, str]] = [ (label, key) for key, label in PARAM_BUCKETS.items() ] # Tab spec: (pillar_key, tab_label, css_class_suffix) _TAB_SPECS: list[tuple[str, str, str]] = [ ("overall", "Overall", "overall"), ("spatial", "Spatial", "spatial"), ("st", "Spatio-Temporal", "st"), ("temporal", "Temporal", "temporal"), ("semantic", "Semantic", "semantic"), ] # Per-tab contextual description HTML (shown above the table) _TAB_DESCRIPTIONS: dict[str, str] = { "overall": ( "Ranks models by their mean score across four reasoning pillars — " "Semantic, Spatial, Spatio-Temporal, and Temporal. " "Click any row to view the per-task breakdown." ), "spatial": ( "Evaluates precise geometric grounding in dense, fixed-camera scenes — " "distinguishing between dozens of identical objects from an elevated, oblique viewpoint." ), "st": ( "Evaluates continuous visual persistence — can a model maintain " "awareness of a specific object across an entire video sequence?" ), "temporal": ( "Evaluates perception of event timing and duration in long, " "untrimmed infrastructure video with extended periods of inactivity." ), "semantic": ( "Evaluates high-level causal and operational reasoning — does the " "model understand what happened and why, not just what is visible?" ), } _TAB_DETAIL_DESC: dict[str, str] = { "overall": ( '
' 'Each pillar score is the mean of its constituent tasks. ' 'Spatial: Referring Expressions · Pointing · Object Localization. ' 'Spatio-Temporal: Single Object Tracking. ' 'Temporal: Temporal Localization · Dense Video Captioning. ' 'Semantic: Event Verification · Video QA.' '
' ), "spatial": ( '
' 'Referring Expressions (mIoU): Localize a target described in natural language — mean IoU between predicted and ground-truth bounding box.
' 'Pointing (Acc): Select the correct 2D coordinate from multiple-choice options — Top-1 accuracy.
' 'Object Localization (F1@0.5): Detect all instances of a given class in the scene — F1 score at IoU threshold 0.5.' '
' ), "st": ( '
' 'SOT (Success AUC): Given a bounding box on frame 1, predict the target\u2019s trajectory across all subsequent frames in a single forward pass. ' 'No rolling memory — pure vision-language inference. ' 'Success AUC = area under the success plot, varying IoU threshold from 0 to 1. ' 'This is the first quantitative VLM tracking benchmark in existence.' '
' ), "temporal": ( '
' 'Temporal Localization (mIoU): Predict the start and end timestamp for a natural language event query — mean IoU between predicted and ground-truth temporal segment.
' 'DVC (SODAc): Autonomously localize and caption all events in chronological order without any query. ' 'SODAc jointly evaluates temporal localization accuracy and semantic caption quality via BERTScore.' '
' ), "semantic": ( '
' 'Event Verification (Macro F1): Verify a hypothesis about a video event as true or false against visual evidence. ' 'Macro F1 penalizes models that exploit majority-class bias.
' 'VQA (Accuracy): Multi-step logical reasoning over untrimmed infrastructure video. Top-1 accuracy on 4-choice MCQ.' '
' ), } _BADGE_LEGEND_HTML = ( '
' '
Legend
' '
' '✓ verified' 'independently verified by VANTAGE-Bench team' 'open' 'open-weight model' 'prop.' 'proprietary / closed model' 'system / pipeline' 'system, pipeline, or orchestration of multiple models' 'single' 'single model result' 'new' 'added in the last 30 days' '
' '
' ) # ── Pillar tab intros (rendered above the filter row on pillar tabs only) ─ _PILLAR_INTRO_HTML: dict[str, str] = { "spatial": ( '
' 'Evaluates precise geometric grounding in dense, fixed-camera scenes, ' 'measuring how well models localize, distinguish, and reason about ' 'objects from elevated and oblique viewpoints.' '
' ), "st": ( '
' 'Evaluates continuous object understanding over time, measuring ' 'whether models can maintain identity and spatial awareness across ' 'motion, occlusion, and scene changes.' '
' ), "temporal": ( '
' 'Evaluates temporal reasoning in camera footage, measuring how well ' 'models localize events and understand activity progression over time.' '
' ), "semantic": ( '
' 'Evaluates high-level semantic understanding and operational reasoning, ' 'measuring how well models interpret events, intent, and activity in ' 'fixed-camera environments.' '
' ), } # ── Below-table descriptions (rendered between table and legend) ────────── # Overall gets a single explanatory paragraph; pillar tabs get a compact # task-definition list. _BELOW_TABLE_HTML: dict[str, str] = { "overall": ( '
' '

Ranks all models by their mean score across four operational ' 'pillars: Spatial, Spatio-Temporal, Temporal, and Semantic. Each ' 'pillar score is computed as the average of its constituent tasks, ' 'measuring overall Infrastructure AI capability. ' 'Click any model row to view detailed model information and ' 'per-task scores.

' '
' ), "spatial": ( '
' '
' '
2D Object Localization
' '
Detect and localize instances of a given class.
' '
2D Referring Expressions
' '
Localize a target described in natural language.
' '
2D Pointing
' '
Select the correct 2D coordinate from multiple choices.
' '
' '
' ), "st": ( '
' '
' '
Single Object Tracking
' '
Track a target object consistently across multiple frames.
' '
' '
' ), "temporal": ( '
' '
' '
Temporal Localization
' '
Predict precise start and end timestamps of events.
' '
Dense Video Captioning
' '
Generate temporally grounded descriptions of chronological events.
' '
' '
' ), "semantic": ( '
' '
' '
Event Verification
' '
Verify whether operational or safety-critical events occurred.
' '
Video QA
' '
Answer multi-choice questions based on video evidence.
' '
' '
' ), } _TABLE_FOOTER_HTML = ( '' ) _ABOUT_TASK_TABLE = """

Task Taxonomy

Task Pillar Metric Description
Grounding Spatial mIoU Referring expression comprehension — localize a target object by natural language
Pointing Spatial Accuracy 2D spatial pointing from multiple-choice coordinate options
Obj. Localization Spatial F1@0.5 Detect all instances of a class in a scene
SOT Spatio-Temporal AUC Single object tracking across frames via pure VLM inference
Temp. Localization Temporal mIoU Predict start/end timestamps for a natural language event query
DVC Temporal SODAc Autonomously localize and caption all events in a video
Event Verification Semantic Macro F1 Verify a hypothesis about a video event as true or false
VQA Semantic Accuracy Multi-step logical reasoning over untrimmed infrastructure video
""" # --------------------------------------------------------------------------- # Resize JS — custom drag handle for each leaderboard table. # # Why JS: Gradio's Svelte `Dataframe` ships scoped CSS that constrains the # real scroll viewport (`.table-wrap`). Browser-native `resize: vertical` # on .table-wrap either gets clobbered by Gradio's own height/flex logic or # resizes a decorative wrapper without growing the row area. So we attach a # visible handle ourselves and write `.table-wrap.style.height` directly on # pointer drag — the actual element that controls how many rows are visible. # # Scoped to `.lb-table` so About / side-panel tables are untouched. Survives # Gradio re-renders (filter changes / tab switches) via a debounced # MutationObserver that re-attaches the handle and re-applies the current # height when the table is replaced or re-styled. # --------------------------------------------------------------------------- _RESIZE_JS = r""" () => { const MIN_H = 280; const MAX_H = 1100; const DEFAULT_H = 480; function applyHeight(lbTable, h) { // .table-wrap is the actual scroll viewport that controls row visibility. lbTable.querySelectorAll('.table-wrap').forEach(function (wrap) { wrap.style.height = h + 'px'; wrap.style.maxHeight = 'none'; wrap.style.minHeight = '0'; wrap.style.overflowY = 'auto'; wrap.style.overflowX = 'auto'; wrap.style.display = 'block'; wrap.style.boxSizing = 'border-box'; }); } function getOrCreateHandle(lbTable) { // Place the handle as a SIBLING right after .lb-table so Gradio's // Svelte tree doesn't manage (and potentially reap) it. var next = lbTable.nextElementSibling; if (next && next.classList && next.classList.contains('lb-resize-handle')) { return next; } var handle = document.createElement('div'); handle.className = 'lb-resize-handle'; handle.setAttribute('role', 'separator'); handle.setAttribute('aria-orientation', 'horizontal'); handle.setAttribute('aria-label', 'Resize leaderboard table'); handle.setAttribute('title', 'Drag to resize table'); lbTable.parentNode.insertBefore(handle, lbTable.nextSibling); return handle; } function attach(lbTable) { if (window.matchMedia('(max-width: 768px)').matches) return; // Skip the Overall tab: it is a raw HTML table sized by content, // not a Gradio Dataframe; it has no .table-wrap and needs no resize handle. if (lbTable.classList.contains('lb-table-overall')) return; var handle = getOrCreateHandle(lbTable); var currentH = handle._lbHeight || DEFAULT_H; handle._lbHeight = currentH; applyHeight(lbTable, currentH); if (handle._lbAttached) return; handle._lbAttached = true; var startY = 0; var startH = 0; var dragging = false; function onMove(e) { if (!dragging) return; var clientY = (e.touches && e.touches[0]) ? e.touches[0].clientY : e.clientY; var dy = clientY - startY; var h = Math.max(MIN_H, Math.min(MAX_H, startH + dy)); handle._lbHeight = h; applyHeight(lbTable, h); } function onUp() { if (!dragging) return; dragging = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); } function onDown(e) { e.preventDefault(); dragging = true; var clientY = (e.touches && e.touches[0]) ? e.touches[0].clientY : e.clientY; startY = clientY; startH = handle._lbHeight; document.body.style.cursor = 'ns-resize'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onUp); } handle.addEventListener('mousedown', onDown); handle.addEventListener('touchstart', onDown, { passive: false }); // Keyboard accessibility: ArrowUp/Down on focused handle. handle.setAttribute('tabindex', '0'); handle.addEventListener('keydown', function (e) { var step = e.shiftKey ? 40 : 16; if (e.key === 'ArrowDown') { handle._lbHeight = Math.min(MAX_H, handle._lbHeight + step); applyHeight(lbTable, handle._lbHeight); e.preventDefault(); } else if (e.key === 'ArrowUp') { handle._lbHeight = Math.max(MIN_H, handle._lbHeight - step); applyHeight(lbTable, handle._lbHeight); e.preventDefault(); } }); } // Row-click bridge for the Overall HTML table. // gr.HTML has no .select event, so we delegate clicks to a // hidden gr.Textbox (#lb-overall-selected-id) whose .change handler in // Python opens the side panel. A timestamp suffix is appended so that // repeat clicks on the same row still register as a value change. function attachOverallClick() { document.querySelectorAll('.lb-table-overall tbody').forEach(function (tbody) { if (tbody._lbOverallClickAttached) return; tbody._lbOverallClickAttached = true; tbody.style.cursor = 'pointer'; tbody.addEventListener('click', function (ev) { var tr = ev.target.closest('tr[data-id]'); if (!tr) return; var modelId = tr.getAttribute('data-id'); if (!modelId) return; var host = document.getElementById('lb-overall-selected-id'); if (!host) return; var input = host.querySelector('input, textarea'); if (!input) return; var proto = (input.tagName === 'TEXTAREA') ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; var nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value').set; nativeSetter.call(input, modelId + '|' + Date.now()); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }); }); } function scan() { document.querySelectorAll('.lb-table').forEach(attach); attachOverallClick(); } scan(); // Settle pass after the initial Gradio render finishes. setTimeout(scan, 300); // Debounced re-scan: filter callbacks and tab switches mutate the DOM and // may strip the inline height from .table-wrap; reapply on each frame. var scheduled = false; var obs = new MutationObserver(function () { if (scheduled) return; scheduled = true; requestAnimationFrame(function () { scheduled = false; scan(); }); }); obs.observe(document.body, { childList: true, subtree: true }); } """ def _col_widths(headers: list[str]) -> list[str]: out = [] for h in headers: if h == "#": out.append("48px") elif h == "Name": out.append("360px") else: out.append("90px") return out def build_demo() -> gr.Blocks: with gr.Blocks( title="VANTAGE-Bench Leaderboard", analytics_enabled=False, fill_width=True, ) as demo: # ── Header ─────────────────────────────────────────────────────── gr.HTML(HEADER_HTML) # ── Leaderboard tabs + static tabs ──────────────────────────────── # df_widgets[0] is a gr.HTML for the Overall tab (grouped header is # rendered as raw HTML because gr.Dataframe does not support multi- # level column headers). Indices 1-4 are gr.Dataframe for pillar tabs. df_widgets: list = [] panel_html_widgets: list[gr.HTML] = [] panel_col_widgets: list[gr.Column] = [] close_btns: list[gr.Button] = [] # Hidden textbox that the JS row-click bridge writes the clicked # model id into; its .change event opens the Overall-tab side panel. overall_select_id: gr.Textbox | None = None tab_filter_groups: list[list] = [] # ── Leaderboard tabs + static tabs ───────────────────────────────── with gr.Tabs(elem_classes=["lb-top-tabs"]): for pillar_key, tab_label, cls_suffix in _TAB_SPECS: with gr.Tab(tab_label): hdrs = headers_for_tab(pillar_key) if pillar_key in _PILLAR_INTRO_HTML: gr.HTML( _PILLAR_INTRO_HTML[pillar_key], elem_classes=["lb-pillar-desc-wrap"], ) with gr.Row(elem_classes=["lb-filters"]): with gr.Column(scale=2, min_width=200, elem_classes=["lb-filter-col"]): gr.HTML('Search') tab_search = gr.Textbox( placeholder="Search by model name or organization…", show_label=False, container=False, elem_classes=["lb-search-box"], ) with gr.Column(scale=1, min_width=120, elem_classes=["lb-filter-col"]): gr.HTML('Submission Type') tab_type_dd = gr.Dropdown( choices=["All", "Single", "System / Pipeline"], value="All", show_label=False, filterable=False, interactive=True, container=False, ) with gr.Column(scale=1, min_width=120, elem_classes=["lb-filter-col"]): gr.HTML('Access') tab_access_dd = gr.Dropdown( choices=["All", "Open-weight", "Proprietary"], value="All", show_label=False, filterable=False, interactive=True, container=False, ) with gr.Column(scale=1, min_width=120, elem_classes=["lb-filter-col"]): gr.HTML('Parameters') tab_params_dd = gr.Dropdown( choices=_PARAMS_CHOICES, value="all", show_label=False, filterable=False, interactive=True, container=False, ) with gr.Column(scale=0, min_width=110, elem_classes=["lb-filter-col", "lb-filter-verified"]): tab_verified_cb = gr.Checkbox( value=False, label="Verified only", container=False, elem_classes=["lb-verified-cb"], ) tab_filter_groups.append([tab_search, tab_type_dd, tab_access_dd, tab_params_dd, tab_verified_cb]) with gr.Row(elem_classes=["lb-table-row"]): with gr.Column(): if pillar_key == "overall": # Hidden bridge: JS writes "{model_id}|{ts}" # here on row click; .change fires the side-panel # callback. See attachOverallClick() in _RESIZE_JS. # NOTE: must be visible=True so Gradio renders the # into the DOM (visible=False is omitted # entirely in Gradio 6.0); CSS in css.py hides # the wrapper visually. overall_select_id = gr.Textbox( value="", show_label=False, container=False, elem_id="lb-overall-selected-id", elem_classes=["lb-overall-hidden-input"], ) df = gr.HTML( value=_INIT_OVERALL_HTML, elem_classes=["lb-table", f"lb-table-{cls_suffix}"], ) else: df = gr.Dataframe( value=_INIT_TABLES[pillar_key], headers=hdrs, datatype=datatypes_for_tab(pillar_key), interactive=False, wrap=False, show_label=False, elem_classes=["lb-table", f"lb-table-{cls_suffix}"], ) df_widgets.append(df) if pillar_key in _BELOW_TABLE_HTML: gr.HTML( _BELOW_TABLE_HTML[pillar_key], elem_classes=["lb-desc-wrap"], ) gr.HTML(_BADGE_LEGEND_HTML, elem_classes=["lb-legend-wrap"]) gr.HTML(_TABLE_FOOTER_HTML, elem_classes=["lb-footer-wrap"]) with gr.Column( scale=0, min_width=280, visible=False, elem_classes=["lb-side-panel"], ) as panel_col: close_btn = gr.Button("✕", elem_classes=["sp-close-btn"]) panel_html = gr.HTML( "", elem_classes=["lb-panel-content"], ) panel_html_widgets.append(panel_html) panel_col_widgets.append(panel_col) close_btns.append(close_btn) with gr.Tab("About"): gr.Markdown(ABOUT_MD, elem_classes=["lb-about"]) # ── Status line ─────────────────────────────────────────────────── status_html = gr.HTML(_INIT_STATUS, elem_classes=["lb-status-wrap"]) # ── Wiring: filter controls → tables + status + panels ──────────── outputs = ( df_widgets # 5 DataFrames + [status_html] # 1 status HTML + panel_html_widgets # 5 panel HTML strings + panel_col_widgets # 5 panel Column visibility ) for group in tab_filter_groups: for ctrl in group: ctrl.change( fn=on_filter_change, inputs=group, outputs=outputs, ) # ── Row-click: open side panel ───────────────────────────────────── # Pillar tabs (gr.Dataframe) use Gradio's built-in .select event. # The Overall tab (gr.HTML) has no .select event, so it uses a JS-to- # Gradio bridge: a hidden gr.Textbox receives the clicked row's # data-id and its .change fires the same kind of side-panel update. for df, ph, pc in zip(df_widgets, panel_html_widgets, panel_col_widgets): if isinstance(df, gr.Dataframe): df.select( fn=_on_row_select, inputs=[df], outputs=[ph, pc], ) if overall_select_id is not None: overall_select_id.change( fn=_on_overall_row_click_id, inputs=[overall_select_id], outputs=[panel_html_widgets[0], panel_col_widgets[0]], ) # ── Close buttons: one per tab ────────────────────────────────────── for close_btn, ph, pc in zip(close_btns, panel_html_widgets, panel_col_widgets): close_btn.click( fn=lambda: ("", gr.update(visible=False)), outputs=[ph, pc], ) return demo demo = build_demo() if __name__ == "__main__": # NOTE: in Gradio 6.0 the `js` and `css` arguments live on launch(), # not on Blocks(); passing them to Blocks silently no-ops. # `js=` proved unreliable in 6.0, so the same code is also injected as # a real " ) demo.launch(css=CSS, js=_RESIZE_JS, head=_head_script, theme=gr.themes.Default())