From 4172f9b279d3da805b53bdb0351c9dd41e94c3a7 Mon Sep 17 00:00:00 2001 From: "joseph.marinier" Date: Fri, 10 Apr 2026 16:45:11 -0400 Subject: [PATCH 1/4] Update Streamlit to 1.56 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78cf1fa8..9f59b76c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dev = [ apps = [ "pandas>=2.0", "plotly>=5.0", - "streamlit>=1.51.0", + "streamlit>=1.56.0", "streamlit-diff-viewer>=0.0.2", ] diff --git a/uv.lock b/uv.lock index a291ea54..fafa81dc 100644 --- a/uv.lock +++ b/uv.lock @@ -846,7 +846,7 @@ requires-dist = [ { name = "regex", specifier = ">=2023.0.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, { name = "setuptools", specifier = ">=65.0.0" }, - { name = "streamlit", marker = "extra == 'apps'", specifier = ">=1.51.0" }, + { name = "streamlit", marker = "extra == 'apps'", specifier = ">=1.56.0" }, { name = "streamlit-diff-viewer", marker = "extra == 'apps'", specifier = ">=0.0.2" }, { name = "structlog", specifier = ">=23.0" }, { name = "tqdm", specifier = ">=4.65" }, @@ -3247,7 +3247,7 @@ wheels = [ [[package]] name = "streamlit" -version = "1.54.0" +version = "1.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altair" }, @@ -3269,9 +3269,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "watchdog", marker = "sys_platform != 'darwin'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/66/d887ee80ea85f035baee607c60af024994e17ae9b921277fca9675e76ecf/streamlit-1.54.0.tar.gz", hash = "sha256:09965e6ae7eb0357091725de1ce2a3f7e4be155c2464c505c40a3da77ab69dd8", size = 8662292, upload-time = "2026-02-04T16:37:54.734Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/85/7c669b3a1336d34ef39fa9760fbd343185f3b15db2ad0838fd78423d1c7f/streamlit-1.56.0.tar.gz", hash = "sha256:1176acfa89ae1318b79078e8efe689a9d02e8d58e325c00fc0e55fa2f3fe8d6a", size = 8559239, upload-time = "2026-03-31T22:29:38.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/1d/40de1819374b4f0507411a60f4d2de0d620a9b10c817de5925799132b6c9/streamlit-1.54.0-py3-none-any.whl", hash = "sha256:a7b67d6293a9f5f6b4d4c7acdbc4980d7d9f049e78e404125022ecb1712f79fc", size = 9119730, upload-time = "2026-02-04T16:37:52.199Z" }, + { url = "https://files.pythonhosted.org/packages/e4/91/cb6f13a89e376ef179309d74f37a70ea0041d5e4b5ba5c4836dbf6e020ad/streamlit-1.56.0-py3-none-any.whl", hash = "sha256:8677a335734a30a51bc57ad0ec910e365d95f7c456fc02c60032927cd0729dc5", size = 9052089, upload-time = "2026-03-31T22:29:36.342Z" }, ] [[package]] From 1efff2aff21d299ee22f70c1c418c384d13eaa6c Mon Sep 17 00:00:00 2001 From: "joseph.marinier" Date: Fri, 10 Apr 2026 17:58:43 -0400 Subject: [PATCH 2/4] Bind query params with the new bind feature instead of manually --- apps/analysis.py | 97 ++++++++---------------------------------------- 1 file changed, 16 insertions(+), 81 deletions(-) diff --git a/apps/analysis.py b/apps/analysis.py index af4b1460..945de05f 100644 --- a/apps/analysis.py +++ b/apps/analysis.py @@ -875,45 +875,12 @@ def render_cross_run_comparison(run_dirs: list[Path], output_dir_str: str = ""): all_providers.update(_extract_providers(cfg)) all_types.add(_classify_pipeline_type(cfg)) - # Read defaults from query params - qp = st.query_params - default_models = [m for m in qp.get_all("model") if m in all_models] or sorted(all_models) - default_providers = [p for p in qp.get_all("provider") if p in all_providers] or sorted(all_providers) - default_types = [t for t in qp.get_all("type") if t in all_types] or sorted(all_types) - # Render filters - sel_types = st.multiselect("Pipeline Type", sorted(all_types), default=default_types) - sel_providers = st.multiselect("Provider", sorted(all_providers), default=default_providers) - sel_models = st.multiselect("Model", sorted(all_models), default=default_models) - - # Update query params only when they differ to avoid rerun loops - new_params: dict[str, str | list[str]] = { - "output_dir": output_dir_str or (str(run_dirs[0].parent) if run_dirs else ""), - "view": "Cross-Run Comparison", - } - if sel_models and set(sel_models) != all_models: - new_params["model"] = sel_models - if sel_providers and set(sel_providers) != all_providers: - new_params["provider"] = sel_providers - if sel_types and set(sel_types) != all_types: - new_params["type"] = sel_types - - # Compare with current params to avoid unnecessary rerun - current = dict(st.query_params) - needs_update = False - for k, v in new_params.items(): - cur_val = current.get(k) - if isinstance(v, list): - if sorted(v) != sorted(st.query_params.get_all(k)): - needs_update = True - break - elif cur_val != v: - needs_update = True - break - if set(current.keys()) != set(new_params.keys()): - needs_update = True - if needs_update: - st.query_params.from_dict(new_params) + sel_types = st.multiselect("Pipeline Type", sorted(all_types), default=all_types, key="type", bind="query-params") + sel_providers = st.multiselect( + "Provider", sorted(all_providers), default=all_providers, key="provider", bind="query-params" + ) + sel_models = st.multiselect("Model", sorted(all_models), default=all_models, key="model", bind="query-params") # Apply filters filtered_dirs = [ @@ -1749,12 +1716,9 @@ def main(): st.set_page_config(page_title="EVA Results Analysis", layout="wide") st.title("EVA Results Analysis") - query_params = st.query_params - - # Sidebar: output directory selection - st.sidebar.header("Output Directory") - default_output = query_params.get("output_dir", _DEFAULT_OUTPUT_DIR) - output_dir = Path(st.sidebar.text_input("Path to output directory", value=default_output)) + output_dir = Path( + st.sidebar.text_input("Output directory", value=_DEFAULT_OUTPUT_DIR, key="output_dir", bind="query-params") + ) run_dirs = get_run_directories(output_dir) @@ -1764,10 +1728,8 @@ def main(): # View mode st.sidebar.header("View") - default_view = query_params.get("view", "Cross-Run Comparison") view_options = ["Cross-Run Comparison", "Run Overview", "Record Detail"] - default_view_idx = view_options.index(default_view) if default_view in view_options else 0 - view_mode = st.sidebar.radio("View", view_options, index=default_view_idx, label_visibility="collapsed") + view_mode = st.sidebar.radio("View", view_options, key="view", label_visibility="collapsed", bind="query-params") if view_mode == "Cross-Run Comparison": render_cross_run_comparison([output_dir / d.name for d in run_dirs], str(output_dir)) @@ -1775,27 +1737,17 @@ def main(): # Sidebar: run selection st.sidebar.header("Run Selection") - run_dir_names = [d.name for d in run_dirs] - default_run_idx = 0 - if "run" in query_params and query_params["run"] in run_dir_names: - default_run_idx = run_dir_names.index(query_params["run"]) - selected_run_name = st.sidebar.selectbox("Select Run", run_dir_names, index=default_run_idx) - selected_run_dir = output_dir / selected_run_name + selected_run_dir = st.sidebar.selectbox( + "Select Run", run_dirs, format_func=lambda d: d.name, key="run", bind="query-params" + ) run_config = _load_run_config(selected_run_dir) if run_config: - _render_sidebar_run_metadata(selected_run_name, run_config) + _render_sidebar_run_metadata(selected_run_dir.name, run_config) else: - st.sidebar.info(f"**Run:** {selected_run_name}") + st.sidebar.info(f"**Run:** {selected_run_dir.name}") if view_mode == "Run Overview": - st.query_params.from_dict( - { - "output_dir": str(output_dir), - "view": "Run Overview", - "run": selected_run_name, - } - ) render_run_overview(selected_run_dir) return @@ -1809,10 +1761,7 @@ def main(): # Sidebar: record selection st.sidebar.header("Record Selection") record_names = [d.name for d in record_dirs] - default_record_idx = 0 - if "record" in query_params and query_params["record"] in record_names: - default_record_idx = record_names.index(query_params["record"]) - selected_record_name = st.sidebar.selectbox("Select Record", record_names, index=default_record_idx) + selected_record_name = st.sidebar.selectbox("Select Record", record_names, key="record", bind="query-params") selected_record_dir = selected_run_dir / "records" / selected_record_name # Detect trial subdirectories @@ -1832,23 +1781,9 @@ def main(): selected_trial = None if trial_dirs: trial_names = [d.name for d in trial_dirs] - default_trial_idx = 0 - if "trial" in query_params and query_params["trial"] in trial_names: - default_trial_idx = trial_names.index(query_params["trial"]) - selected_trial = st.sidebar.selectbox("Select Trial", trial_names, index=default_trial_idx) + selected_trial = st.sidebar.selectbox("Select Trial", trial_names, key="trial", bind="query-params") selected_record_dir = selected_record_dir / selected_trial - # Update query params for deep linking - new_params = { - "output_dir": str(output_dir), - "view": "Record Detail", - "run": selected_run_name, - "record": selected_record_name, - } - if selected_trial: - new_params["trial"] = selected_trial - st.query_params.from_dict(new_params) - # Load data result = load_record_result(selected_record_dir) metrics = load_record_metrics(selected_record_dir) From 3636a53db0243b794bf20037732d1f6ed447763f Mon Sep 17 00:00:00 2001 From: "joseph.marinier" Date: Fri, 10 Apr 2026 18:50:42 -0400 Subject: [PATCH 3/4] Use pages instead of radio buttons --- apps/analysis.py | 67 +++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/apps/analysis.py b/apps/analysis.py index 945de05f..856b4f24 100644 --- a/apps/analysis.py +++ b/apps/analysis.py @@ -858,7 +858,7 @@ def _render_eva_scatter_plot(scatter_data: list[dict]): # ============================================================================ -def render_cross_run_comparison(run_dirs: list[Path], output_dir_str: str = ""): +def render_cross_run_comparison(run_dirs: list[Path]): """Render a comparison view across multiple runs.""" st.markdown("### Cross-Run Comparison") st.caption("Compare aggregate metrics across all runs that have metrics data.") @@ -1001,12 +1001,7 @@ def render_cross_run_comparison(run_dirs: list[Path], output_dir_str: str = ""): display_df = summary_df[display_cols].copy() # Add link column to navigate to Run Overview - output_dir_str = str(run_dirs[0].parent) if run_dirs else "" - display_df.insert( - 0, - "link", - summary_df["run"].apply(lambda r: f"?output_dir={output_dir_str}&view=Run+Overview&run={r}"), - ) + display_df.insert(0, "link", f"/run_overview?output_dir={run_dirs[0].parent}&run=" + summary_df["run"]) composite_rename = {c: f"[EVA] {_EVA_COMPOSITE_DISPLAY[c]}" for c in table_composites} display_df = display_df.rename(columns={"label": "Run", "records": "# Records", **composite_rename, **col_rename}) @@ -1707,15 +1702,8 @@ def _render_sidebar_run_metadata(run_name: str, run_config: dict): st.sidebar.info("\n\n".join(metadata_parts)) -# ============================================================================ -# Main App -# ============================================================================ - - -def main(): - st.set_page_config(page_title="EVA Results Analysis", layout="wide") - st.title("EVA Results Analysis") - +def _get_run_dirs(): + """Get run directories, showing an error if none found.""" output_dir = Path( st.sidebar.text_input("Output directory", value=_DEFAULT_OUTPUT_DIR, key="output_dir", bind="query-params") ) @@ -1724,18 +1712,12 @@ def main(): if not run_dirs: st.error(f"No run directories found in {output_dir}") - return + st.stop() - # View mode - st.sidebar.header("View") - view_options = ["Cross-Run Comparison", "Run Overview", "Record Detail"] - view_mode = st.sidebar.radio("View", view_options, key="view", label_visibility="collapsed", bind="query-params") + return run_dirs - if view_mode == "Cross-Run Comparison": - render_cross_run_comparison([output_dir / d.name for d in run_dirs], str(output_dir)) - return - # Sidebar: run selection +def _select_run(run_dirs: list[Path]): st.sidebar.header("Run Selection") selected_run_dir = st.sidebar.selectbox( "Select Run", run_dirs, format_func=lambda d: d.name, key="run", bind="query-params" @@ -1747,11 +1729,10 @@ def main(): else: st.sidebar.info(f"**Run:** {selected_run_dir.name}") - if view_mode == "Run Overview": - render_run_overview(selected_run_dir) - return + return selected_run_dir - # Record detail view + +def render_record_detail(selected_run_dir: Path): record_dirs = get_record_directories(selected_run_dir) if not record_dirs: @@ -1886,5 +1867,33 @@ def main(): render_processed_data_tab(metrics) +# ============================================================================ +# Main App +# ============================================================================ + + +def cross_run_comparison(): + render_cross_run_comparison(_get_run_dirs()) + + +def run_overview(): + render_run_overview(_select_run(_get_run_dirs())) + + +def record_detail(): + render_record_detail(_select_run(_get_run_dirs())) + + +def main(): + st.set_page_config(page_title="EVA Results Analysis", layout="wide") + + pages = ( + st.Page(cross_run_comparison, title="Cross-Run Comparison", icon=":material/compare_arrows:"), + st.Page(run_overview, title="Run Overview", icon=":material/summarize:"), + st.Page(record_detail, title="Record Detail", icon=":material/article:"), + ) + st.navigation(pages).run() + + if __name__ == "__main__": main() From 7a350ccfd809a767eb80ffd44c75c4d66d14c625 Mon Sep 17 00:00:00 2001 From: "joseph.marinier" Date: Fri, 10 Apr 2026 18:51:31 -0400 Subject: [PATCH 4/4] Add EVA favicon --- apps/analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/analysis.py b/apps/analysis.py index 856b4f24..4e651752 100644 --- a/apps/analysis.py +++ b/apps/analysis.py @@ -1885,7 +1885,7 @@ def record_detail(): def main(): - st.set_page_config(page_title="EVA Results Analysis", layout="wide") + st.set_page_config(page_title="EVA Results Analysis", layout="wide", page_icon="website/public/favicon.svg") pages = ( st.Page(cross_run_comparison, title="Cross-Run Comparison", icon=":material/compare_arrows:"),