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'
Model info
'
f'
{info_cells}
'
f'
Pillar scores
'
f'
{radar_svg}
'
f'
All task scores
'
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'
'
'
'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.
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())