Deploy H2EPR-Bench Explorer Space
Browse files- Dockerfile +16 -0
- README.md +26 -4
- app.py +126 -0
- requirements.txt +5 -0
- src/h2epr_explorer/__init__.py +2 -0
- src/h2epr_explorer/constants.py +29 -0
- src/h2epr_explorer/data_loader.py +126 -0
- src/h2epr_explorer/filters.py +41 -0
- src/h2epr_explorer/render_gantt.py +76 -0
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
ENV PIP_NO_CACHE_DIR=1
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY requirements.txt /app/requirements.txt
|
| 10 |
+
RUN pip install --upgrade pip && pip install -r /app/requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY . /app
|
| 13 |
+
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
CMD streamlit run app.py --server.port=7860 --server.address=0.0.0.0
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title: H2EPR
|
| 3 |
-
emoji: 👀
|
| 4 |
colorFrom: gray
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: H2EPR-Bench Explorer
|
|
|
|
| 3 |
colorFrom: gray
|
| 4 |
+
colorTo: blue
|
| 5 |
sdk: docker
|
| 6 |
pinned: false
|
| 7 |
+
license: cc-by-nc-4.0
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# H²EPR-Bench Explorer
|
| 11 |
+
|
| 12 |
+
H²EPR-Bench Explorer is the planned interactive browsing layer for `AgenticFinLab/H2EPR-Bench`. It is separate from the canonical dataset repository: the dataset repo remains the release package, while this Docker Space runs a Streamlit app for search, event detail, stage inspection, public FinalCascade JSON browsing, and Gantt-style timelines.
|
| 13 |
+
|
| 14 |
+
**Release boundary:** public records are intended for browsing, reuse, and presentation. Official scoring uses the [manual-gated Gold companion](https://huggingface.co/datasets/AgenticFinLab/H2EPR-Bench-Gold). Public FinalCascade and Gantt views are supplementary inspection assets, not official scoring references.
|
| 15 |
+
|
| 16 |
+
## Data Sources
|
| 17 |
+
|
| 18 |
+
| Source | Role |
|
| 19 |
+
|---|---|
|
| 20 |
+
| `AgenticFinLab/H2EPR-Bench` | Public event catalog, stage table, public sanitized FinalCascade, and Gantt artifact paths. |
|
| 21 |
+
| `AgenticFinLab/H2EPR-Bench-Gold` | Manual-gated Gold companion for official scoring references. Linked for users who need scoring access; not loaded by this app. |
|
| 22 |
+
|
| 23 |
+
## Local Development
|
| 24 |
+
|
| 25 |
+
The app can run from a local staged dataset package before upload:
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
export H2EPR_EXPLORER_LOCAL_DATASET_DIR=../../build/hf_dataset_repo_staging/eventmycelium-v1_1000-public
|
| 29 |
+
streamlit run app.py
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Without `H2EPR_EXPLORER_LOCAL_DATASET_DIR`, the app downloads public files from the Hugging Face dataset repo.
|
app.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
APP_ROOT = Path(__file__).resolve().parent
|
| 8 |
+
sys.path.insert(0, str(APP_ROOT / "src"))
|
| 9 |
+
|
| 10 |
+
import streamlit as st
|
| 11 |
+
|
| 12 |
+
from h2epr_explorer.constants import (
|
| 13 |
+
CATALOG_COLUMNS,
|
| 14 |
+
GOLD_COMPANION_REPO,
|
| 15 |
+
PUBLIC_DATASET_REPO,
|
| 16 |
+
RELEASE_BOUNDARY_NOTICE,
|
| 17 |
+
)
|
| 18 |
+
from h2epr_explorer.data_loader import load_catalog, load_event_graph, load_finalcascade_summary, load_stages
|
| 19 |
+
from h2epr_explorer.filters import filter_catalog
|
| 20 |
+
from h2epr_explorer.render_gantt import build_timeline_figure
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _as_records(frame):
|
| 24 |
+
return frame.to_dict(orient="records")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _select_columns(frame, columns):
|
| 28 |
+
present = [column for column in columns if column in frame.columns]
|
| 29 |
+
return frame[present] if present else frame
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
st.set_page_config(page_title="H2EPR-Bench Explorer", layout="wide")
|
| 33 |
+
|
| 34 |
+
st.title("H²EPR-Bench Explorer")
|
| 35 |
+
st.caption("Interactive browser for public event metadata, stage rows, FinalCascade summaries, and Gantt-style views.")
|
| 36 |
+
st.info(RELEASE_BOUNDARY_NOTICE)
|
| 37 |
+
|
| 38 |
+
catalog = load_catalog()
|
| 39 |
+
stages = load_stages()
|
| 40 |
+
summary = load_finalcascade_summary()
|
| 41 |
+
|
| 42 |
+
catalog_rows = _as_records(catalog)
|
| 43 |
+
|
| 44 |
+
with st.sidebar:
|
| 45 |
+
st.header("Filter events")
|
| 46 |
+
query = st.text_input("Search", placeholder="event name, ID, category, keyword")
|
| 47 |
+
domains = st.multiselect("Domain", sorted(catalog["domain"].dropna().unique().tolist()))
|
| 48 |
+
categories = st.multiselect("Category", sorted(catalog["event_category"].dropna().unique().tolist()))
|
| 49 |
+
min_source_count = st.slider("Minimum sources", 0, int(catalog["source_count"].max()), 0)
|
| 50 |
+
min_stage_count = st.slider("Minimum stages", 0, int(catalog["stage_count"].max()), 0)
|
| 51 |
+
|
| 52 |
+
filtered_rows = filter_catalog(
|
| 53 |
+
catalog_rows,
|
| 54 |
+
query=query,
|
| 55 |
+
domains=domains,
|
| 56 |
+
categories=categories,
|
| 57 |
+
min_source_count=min_source_count,
|
| 58 |
+
min_stage_count=min_stage_count,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if not filtered_rows:
|
| 62 |
+
st.warning("No event matches the current filters.")
|
| 63 |
+
st.stop()
|
| 64 |
+
|
| 65 |
+
selected_event = st.selectbox(
|
| 66 |
+
"Selected event",
|
| 67 |
+
[row["event_id"] for row in filtered_rows],
|
| 68 |
+
format_func=lambda event_id: f"{event_id} · {catalog.loc[catalog['event_id'] == event_id, 'event_name'].iloc[0]}",
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
event_row = catalog[catalog["event_id"] == selected_event].iloc[0]
|
| 72 |
+
event_stages = stages[stages["event_id"] == selected_event].sort_values("stage_order")
|
| 73 |
+
summary_row = summary[summary["event_id"] == selected_event]
|
| 74 |
+
|
| 75 |
+
tabs = st.tabs(["Catalog", "Event detail", "Timeline", "Stages", "FinalCascade JSON", "Release boundary"])
|
| 76 |
+
|
| 77 |
+
with tabs[0]:
|
| 78 |
+
st.subheader(f"Event catalog: {len(filtered_rows)} of {len(catalog_rows)} events")
|
| 79 |
+
st.dataframe(_select_columns(catalog[catalog["event_id"].isin([row["event_id"] for row in filtered_rows])], CATALOG_COLUMNS), use_container_width=True, height=520)
|
| 80 |
+
|
| 81 |
+
with tabs[1]:
|
| 82 |
+
st.subheader(str(event_row.get("event_name", selected_event)))
|
| 83 |
+
st.write(str(event_row.get("short_description", "")))
|
| 84 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 85 |
+
c1.metric("Domain", str(event_row.get("domain", "")))
|
| 86 |
+
c2.metric("Category", str(event_row.get("event_category", "")))
|
| 87 |
+
c3.metric("Sources", int(event_row.get("source_count", 0)))
|
| 88 |
+
c4.metric("Stages", int(event_row.get("stage_count", 0)))
|
| 89 |
+
if not summary_row.empty:
|
| 90 |
+
st.markdown("#### Public FinalCascade summary")
|
| 91 |
+
st.dataframe(summary_row, use_container_width=True)
|
| 92 |
+
|
| 93 |
+
with tabs[2]:
|
| 94 |
+
figure = build_timeline_figure(_as_records(event_stages), selected_event)
|
| 95 |
+
if figure is None:
|
| 96 |
+
st.warning("No public stage rows are available for this event.")
|
| 97 |
+
else:
|
| 98 |
+
st.plotly_chart(figure, use_container_width=True)
|
| 99 |
+
if "gantt_html_path" in event_row and event_row.get("gantt_html_path"):
|
| 100 |
+
st.markdown(f"Gantt HTML artifact path: `{event_row.get('gantt_html_path')}`")
|
| 101 |
+
|
| 102 |
+
with tabs[3]:
|
| 103 |
+
st.dataframe(event_stages, use_container_width=True, height=520)
|
| 104 |
+
|
| 105 |
+
with tabs[4]:
|
| 106 |
+
graph = load_event_graph(selected_event)
|
| 107 |
+
st.download_button(
|
| 108 |
+
"Download selected public FinalCascade JSON",
|
| 109 |
+
data=json.dumps(graph, ensure_ascii=False, indent=2),
|
| 110 |
+
file_name=f"{selected_event}_finalcascade_public.json",
|
| 111 |
+
mime="application/json",
|
| 112 |
+
)
|
| 113 |
+
st.json(graph, expanded=False)
|
| 114 |
+
|
| 115 |
+
with tabs[5]:
|
| 116 |
+
st.markdown(
|
| 117 |
+
f"""
|
| 118 |
+
### Release boundary
|
| 119 |
+
|
| 120 |
+
- Public dataset repo: [`{PUBLIC_DATASET_REPO}`](https://huggingface.co/datasets/{PUBLIC_DATASET_REPO})
|
| 121 |
+
- Manual-gated Gold companion: [`{GOLD_COMPANION_REPO}`](https://huggingface.co/datasets/{GOLD_COMPANION_REPO})
|
| 122 |
+
- This Explorer loads public event metadata, public stages, public sanitized FinalCascade records, and public visualization paths.
|
| 123 |
+
- It does not load gated Gold references.
|
| 124 |
+
- Public FinalCascade and Gantt views are supplementary inspection assets, not official scoring references.
|
| 125 |
+
"""
|
| 126 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.36
|
| 2 |
+
pandas>=2.0
|
| 3 |
+
pyarrow>=14.0
|
| 4 |
+
plotly>=5.20
|
| 5 |
+
huggingface_hub>=0.23
|
src/h2epr_explorer/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Support modules for the H2EPR-Bench Explorer Space."""
|
| 2 |
+
|
src/h2epr_explorer/constants.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PUBLIC_DATASET_REPO = "AgenticFinLab/H2EPR-Bench"
|
| 2 |
+
GOLD_COMPANION_REPO = "AgenticFinLab/H2EPR-Bench-Gold"
|
| 3 |
+
LOCAL_DATASET_ENV = "H2EPR_EXPLORER_LOCAL_DATASET_DIR"
|
| 4 |
+
|
| 5 |
+
CATALOG_PARQUET = "data/viewer_mirrors/event_catalog.parquet"
|
| 6 |
+
CATALOG_JSONL = "data/event_catalog.jsonl"
|
| 7 |
+
STAGES_PARQUET = "data/viewer_mirrors/event_stages.parquet"
|
| 8 |
+
STAGES_JSONL = "data/event_stages.jsonl"
|
| 9 |
+
FINALCASCADE_JSONL = "data/finmycelium_finalcascade_public.jsonl"
|
| 10 |
+
FINALCASCADE_SUMMARY_PARQUET = "data/viewer_mirrors/finalcascade_summary.parquet"
|
| 11 |
+
|
| 12 |
+
RELEASE_BOUNDARY_NOTICE = (
|
| 13 |
+
"Official scoring uses the manual-gated Gold companion repository. "
|
| 14 |
+
"Public FinalCascade and Gantt views are supplementary inspection assets, "
|
| 15 |
+
"not official scoring references."
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
CATALOG_COLUMNS = [
|
| 19 |
+
"event_id",
|
| 20 |
+
"event_name",
|
| 21 |
+
"domain",
|
| 22 |
+
"event_category",
|
| 23 |
+
"short_description",
|
| 24 |
+
"keywords",
|
| 25 |
+
"source_count",
|
| 26 |
+
"stage_count",
|
| 27 |
+
"gantt_html_path",
|
| 28 |
+
]
|
| 29 |
+
|
src/h2epr_explorer/data_loader.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from functools import lru_cache
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
from .constants import (
|
| 10 |
+
CATALOG_JSONL,
|
| 11 |
+
CATALOG_PARQUET,
|
| 12 |
+
FINALCASCADE_JSONL,
|
| 13 |
+
FINALCASCADE_SUMMARY_PARQUET,
|
| 14 |
+
LOCAL_DATASET_ENV,
|
| 15 |
+
PUBLIC_DATASET_REPO,
|
| 16 |
+
STAGES_JSONL,
|
| 17 |
+
STAGES_PARQUET,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class SimpleTable:
|
| 22 |
+
"""Small fallback table used when pandas is unavailable in local checks."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, rows: list[dict[str, Any]]):
|
| 25 |
+
self._rows = rows
|
| 26 |
+
|
| 27 |
+
def __len__(self) -> int:
|
| 28 |
+
return len(self._rows)
|
| 29 |
+
|
| 30 |
+
def to_dict(self, orient: str = "records") -> list[dict[str, Any]]:
|
| 31 |
+
if orient != "records":
|
| 32 |
+
raise ValueError("SimpleTable only supports orient='records'")
|
| 33 |
+
return list(self._rows)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _as_local_root(local_dataset_dir: Path | str | None = None) -> Path | None:
|
| 37 |
+
value = local_dataset_dir or os.environ.get(LOCAL_DATASET_ENV)
|
| 38 |
+
if not value:
|
| 39 |
+
return None
|
| 40 |
+
return Path(value).expanduser().resolve()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def resolve_dataset_file(filename: str, local_dataset_dir: Path | str | None = None) -> Path:
|
| 44 |
+
local_root = _as_local_root(local_dataset_dir)
|
| 45 |
+
if local_root is not None:
|
| 46 |
+
path = (local_root / filename).resolve()
|
| 47 |
+
if not path.is_relative_to(local_root):
|
| 48 |
+
raise ValueError(f"Refusing to read outside local dataset root: {filename}")
|
| 49 |
+
if not path.exists():
|
| 50 |
+
raise FileNotFoundError(path)
|
| 51 |
+
return path
|
| 52 |
+
|
| 53 |
+
from huggingface_hub import hf_hub_download
|
| 54 |
+
|
| 55 |
+
return Path(
|
| 56 |
+
hf_hub_download(
|
| 57 |
+
repo_id=PUBLIC_DATASET_REPO,
|
| 58 |
+
repo_type="dataset",
|
| 59 |
+
filename=filename,
|
| 60 |
+
)
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def load_jsonl_rows(filename: str, local_dataset_dir: Path | str | None = None) -> list[dict[str, Any]]:
|
| 65 |
+
path = resolve_dataset_file(filename, local_dataset_dir=local_dataset_dir)
|
| 66 |
+
rows: list[dict[str, Any]] = []
|
| 67 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 68 |
+
for line in handle:
|
| 69 |
+
line = line.strip()
|
| 70 |
+
if line:
|
| 71 |
+
rows.append(json.loads(line))
|
| 72 |
+
return rows
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def read_event_graph_from_jsonl(
|
| 76 |
+
event_id: str, local_dataset_dir: Path | str | None = None
|
| 77 |
+
) -> dict[str, Any]:
|
| 78 |
+
path = resolve_dataset_file(FINALCASCADE_JSONL, local_dataset_dir=local_dataset_dir)
|
| 79 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 80 |
+
for line in handle:
|
| 81 |
+
if not line.strip():
|
| 82 |
+
continue
|
| 83 |
+
row = json.loads(line)
|
| 84 |
+
if row.get("event_id") == event_id:
|
| 85 |
+
return row
|
| 86 |
+
raise KeyError(f"Event graph not found: {event_id}")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _read_table(filename: str, fallback_jsonl: str, local_dataset_dir: Path | str | None = None):
|
| 90 |
+
pd = None
|
| 91 |
+
try:
|
| 92 |
+
import pandas as pandas_module
|
| 93 |
+
|
| 94 |
+
pd = pandas_module
|
| 95 |
+
except ImportError:
|
| 96 |
+
pass
|
| 97 |
+
try:
|
| 98 |
+
path = resolve_dataset_file(filename, local_dataset_dir=local_dataset_dir)
|
| 99 |
+
if pd is None:
|
| 100 |
+
raise ImportError("pandas is unavailable")
|
| 101 |
+
return pd.read_parquet(path)
|
| 102 |
+
except (FileNotFoundError, ImportError, ValueError):
|
| 103 |
+
rows = load_jsonl_rows(fallback_jsonl, local_dataset_dir=local_dataset_dir)
|
| 104 |
+
if pd is None:
|
| 105 |
+
return SimpleTable(rows)
|
| 106 |
+
return pd.DataFrame(rows)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@lru_cache(maxsize=1)
|
| 110 |
+
def load_catalog():
|
| 111 |
+
return _read_table(CATALOG_PARQUET, CATALOG_JSONL)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@lru_cache(maxsize=1)
|
| 115 |
+
def load_stages():
|
| 116 |
+
return _read_table(STAGES_PARQUET, STAGES_JSONL)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@lru_cache(maxsize=1)
|
| 120 |
+
def load_finalcascade_summary():
|
| 121 |
+
return _read_table(FINALCASCADE_SUMMARY_PARQUET, CATALOG_JSONL)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@lru_cache(maxsize=128)
|
| 125 |
+
def load_event_graph(event_id: str) -> dict[str, Any]:
|
| 126 |
+
return read_event_graph_from_jsonl(event_id)
|
src/h2epr_explorer/filters.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Iterable
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
SEARCH_FIELDS = ("event_id", "event_name", "short_description", "domain", "event_category", "keywords")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _contains_query(row: dict[str, Any], query: str) -> bool:
|
| 10 |
+
if not query:
|
| 11 |
+
return True
|
| 12 |
+
needle = query.casefold()
|
| 13 |
+
return any(needle in str(row.get(field, "")).casefold() for field in SEARCH_FIELDS)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def filter_catalog(
|
| 17 |
+
rows: Iterable[dict[str, Any]],
|
| 18 |
+
*,
|
| 19 |
+
query: str = "",
|
| 20 |
+
domains: list[str] | None = None,
|
| 21 |
+
categories: list[str] | None = None,
|
| 22 |
+
min_source_count: int = 0,
|
| 23 |
+
min_stage_count: int = 0,
|
| 24 |
+
) -> list[dict[str, Any]]:
|
| 25 |
+
domain_set = set(domains or [])
|
| 26 |
+
category_set = set(categories or [])
|
| 27 |
+
filtered: list[dict[str, Any]] = []
|
| 28 |
+
for row in rows:
|
| 29 |
+
if domain_set and row.get("domain") not in domain_set:
|
| 30 |
+
continue
|
| 31 |
+
if category_set and row.get("event_category") not in category_set:
|
| 32 |
+
continue
|
| 33 |
+
if int(row.get("source_count") or 0) < min_source_count:
|
| 34 |
+
continue
|
| 35 |
+
if int(row.get("stage_count") or 0) < min_stage_count:
|
| 36 |
+
continue
|
| 37 |
+
if not _contains_query(row, query):
|
| 38 |
+
continue
|
| 39 |
+
filtered.append(row)
|
| 40 |
+
return filtered
|
| 41 |
+
|
src/h2epr_explorer/render_gantt.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _is_known_time(value: Any) -> bool:
|
| 7 |
+
return bool(value) and str(value).strip().lower() not in {"unknown", "none", "nan", "nat"}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def prepare_gantt_rows(stage_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 11 |
+
ordered = sorted(stage_rows, key=lambda row: (int(row.get("stage_order") or 0), str(row.get("stage_id", ""))))
|
| 12 |
+
prepared: list[dict[str, Any]] = []
|
| 13 |
+
for fallback_index, row in enumerate(ordered, start=1):
|
| 14 |
+
start = row.get("stage_start_time")
|
| 15 |
+
end = row.get("stage_end_time")
|
| 16 |
+
if _is_known_time(start) and _is_known_time(end):
|
| 17 |
+
display_start = start
|
| 18 |
+
display_end = end
|
| 19 |
+
axis_mode = "calendar"
|
| 20 |
+
else:
|
| 21 |
+
display_start = int(row.get("stage_order") or fallback_index)
|
| 22 |
+
display_end = display_start + 0.85
|
| 23 |
+
axis_mode = "relative_order"
|
| 24 |
+
|
| 25 |
+
time_note = row.get("temporal_anchor_summary") or ""
|
| 26 |
+
if not time_note and int(row.get("known_action_time_anchor_count") or 0) > 0:
|
| 27 |
+
time_note = "Action-level time anchors available"
|
| 28 |
+
|
| 29 |
+
prepared.append(
|
| 30 |
+
{
|
| 31 |
+
**row,
|
| 32 |
+
"display_start": display_start,
|
| 33 |
+
"display_end": display_end,
|
| 34 |
+
"axis_mode": axis_mode,
|
| 35 |
+
"time_note": time_note,
|
| 36 |
+
}
|
| 37 |
+
)
|
| 38 |
+
return prepared
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def build_timeline_figure(stage_rows: list[dict[str, Any]], event_id: str):
|
| 42 |
+
import pandas as pd
|
| 43 |
+
import plotly.express as px
|
| 44 |
+
|
| 45 |
+
prepared = prepare_gantt_rows(stage_rows)
|
| 46 |
+
if not prepared:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
frame = pd.DataFrame(prepared)
|
| 50 |
+
if set(frame["axis_mode"]) == {"calendar"}:
|
| 51 |
+
fig = px.timeline(
|
| 52 |
+
frame,
|
| 53 |
+
x_start="display_start",
|
| 54 |
+
x_end="display_end",
|
| 55 |
+
y="stage_label_public",
|
| 56 |
+
color="stage_label_public",
|
| 57 |
+
hover_data=["stage_id", "stage_order", "time_note"],
|
| 58 |
+
title=f"{event_id}: public stage timeline",
|
| 59 |
+
)
|
| 60 |
+
fig.update_yaxes(autorange="reversed")
|
| 61 |
+
return fig
|
| 62 |
+
|
| 63 |
+
fig = px.bar(
|
| 64 |
+
frame,
|
| 65 |
+
x=[row["display_end"] - row["display_start"] for row in prepared],
|
| 66 |
+
y="stage_label_public",
|
| 67 |
+
base="display_start",
|
| 68 |
+
orientation="h",
|
| 69 |
+
color="stage_label_public",
|
| 70 |
+
hover_data=["stage_id", "stage_order", "time_note"],
|
| 71 |
+
title=f"{event_id}: relative stage order",
|
| 72 |
+
)
|
| 73 |
+
fig.update_yaxes(autorange="reversed")
|
| 74 |
+
fig.update_layout(xaxis_title="Relative stage order")
|
| 75 |
+
return fig
|
| 76 |
+
|