wandler67 commited on
Commit
62cba3a
·
verified ·
1 Parent(s): e6d063b

Improve H2EPR Explorer event navigation and timeline UX

Browse files
README.md CHANGED
@@ -9,7 +9,7 @@ license: cc-by-nc-4.0
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
 
 
9
 
10
  # H²EPR-Bench Explorer
11
 
12
+ H²EPR-Bench Explorer is the 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
 
app.py CHANGED
@@ -17,6 +17,12 @@ from h2epr_explorer.constants import (
17
  )
18
  from h2epr_explorer.data_loader import load_catalog, load_event_graph, load_finalcascade_summary, load_stages
19
  from h2epr_explorer.filters import event_description, event_display_label, event_name, filter_catalog
 
 
 
 
 
 
20
  from h2epr_explorer.render_gantt import build_timeline_figure
21
 
22
 
@@ -29,10 +35,57 @@ def _select_columns(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()
@@ -48,6 +101,9 @@ with st.sidebar:
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,
@@ -63,35 +119,80 @@ if not filtered_rows:
63
  st.stop()
64
 
65
  event_labels = {row["event_id"]: event_display_label(row) for row in catalog_rows}
 
 
66
 
67
  selected_event = st.selectbox(
68
  "Selected event",
69
  [row["event_id"] for row in filtered_rows],
 
70
  format_func=lambda event_id: event_labels.get(event_id, event_id),
71
  )
 
 
 
 
72
 
73
  event_row = catalog[catalog["event_id"] == selected_event].iloc[0]
74
  event_record = event_row.to_dict()
75
- event_stages = stages[stages["event_id"] == selected_event].sort_values("stage_order")
76
  summary_row = summary[summary["event_id"] == selected_event]
 
77
 
78
- tabs = st.tabs(["Catalog", "Event detail", "Timeline", "Stages", "FinalCascade JSON", "Release boundary"])
 
 
79
 
80
  with tabs[0]:
81
- st.subheader(f"Event catalog: {len(filtered_rows)} of {len(catalog_rows)} events")
82
  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)
83
 
84
  with tabs[1]:
85
  st.subheader(event_name(event_record))
86
  st.write(event_description(event_record))
87
- c1, c2, c3, c4 = st.columns(4)
88
- c1.metric("Domain", str(event_row.get("domain", "")))
89
- c2.metric("Category", str(event_row.get("event_category", "")))
90
- c3.metric("Sources", int(event_row.get("source_count", 0)))
91
- c4.metric("Stages", int(event_row.get("stage_count", 0)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if not summary_row.empty:
93
  st.markdown("#### Public FinalCascade summary")
94
- st.dataframe(summary_row, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  with tabs[2]:
97
  figure = build_timeline_figure(_as_records(event_stages), selected_event)
 
17
  )
18
  from h2epr_explorer.data_loader import load_catalog, load_event_graph, load_finalcascade_summary, load_stages
19
  from h2epr_explorer.filters import event_description, event_display_label, event_name, filter_catalog
20
+ from h2epr_explorer.navigation import (
21
+ build_event_links,
22
+ filter_summary_text,
23
+ query_param_event_id,
24
+ resolve_selected_event_index,
25
+ )
26
  from h2epr_explorer.render_gantt import build_timeline_figure
27
 
28
 
 
35
  return frame[present] if present else frame
36
 
37
 
38
+ def _sort_stage_frame(frame):
39
+ sort_columns = [column for column in ("stage_index", "stage_order", "stage_id") if column in frame.columns]
40
+ return frame.sort_values(sort_columns) if sort_columns else frame
41
+
42
+
43
+ def _safe_int(value, default=0):
44
+ try:
45
+ return int(value)
46
+ except (TypeError, ValueError):
47
+ return default
48
+
49
+
50
  st.set_page_config(page_title="H2EPR-Bench Explorer", layout="wide")
51
 
52
+ st.markdown(
53
+ """
54
+ <style>
55
+ div[data-testid="stMetric"] {
56
+ border: 1px solid #e5e7eb;
57
+ border-radius: 8px;
58
+ padding: 0.35rem 0.6rem;
59
+ background: #fbfbf8;
60
+ }
61
+ .h2epr-kicker {
62
+ color: #4b5563;
63
+ font-size: 0.92rem;
64
+ letter-spacing: 0;
65
+ margin-bottom: 0.25rem;
66
+ }
67
+ .h2epr-title {
68
+ font-size: 2.15rem;
69
+ font-weight: 760;
70
+ line-height: 1.12;
71
+ margin-bottom: 0.25rem;
72
+ }
73
+ .h2epr-subtitle {
74
+ color: #374151;
75
+ max-width: 920px;
76
+ margin-bottom: 0.75rem;
77
+ }
78
+ </style>
79
+ """,
80
+ unsafe_allow_html=True,
81
+ )
82
+
83
+ st.markdown('<div class="h2epr-kicker">H²EPR-Bench · public release explorer</div>', unsafe_allow_html=True)
84
+ st.markdown('<div class="h2epr-title">Event-process graph browser</div>', unsafe_allow_html=True)
85
+ st.markdown(
86
+ '<div class="h2epr-subtitle">Browse public event metadata, stage rows, FinalCascade summaries, and Gantt-style timelines for the H²EPR-Bench release.</div>',
87
+ unsafe_allow_html=True,
88
+ )
89
  st.info(RELEASE_BOUNDARY_NOTICE)
90
 
91
  catalog = load_catalog()
 
101
  categories = st.multiselect("Category", sorted(catalog["event_category"].dropna().unique().tolist()))
102
  min_source_count = st.slider("Minimum sources", 0, int(catalog["source_count"].max()), 0)
103
  min_stage_count = st.slider("Minimum stages", 0, int(catalog["stage_count"].max()), 0)
104
+ st.divider()
105
+ st.link_button("Dataset repository", f"https://huggingface.co/datasets/{PUBLIC_DATASET_REPO}", use_container_width=True)
106
+ st.link_button("Request Gold access", f"https://huggingface.co/datasets/{GOLD_COMPANION_REPO}", use_container_width=True)
107
 
108
  filtered_rows = filter_catalog(
109
  catalog_rows,
 
119
  st.stop()
120
 
121
  event_labels = {row["event_id"]: event_display_label(row) for row in catalog_rows}
122
+ requested_event_id = query_param_event_id(st.query_params)
123
+ selected_index = resolve_selected_event_index(filtered_rows, requested_event_id)
124
 
125
  selected_event = st.selectbox(
126
  "Selected event",
127
  [row["event_id"] for row in filtered_rows],
128
+ index=selected_index,
129
  format_func=lambda event_id: event_labels.get(event_id, event_id),
130
  )
131
+ try:
132
+ st.query_params["event_id"] = selected_event
133
+ except Exception:
134
+ pass
135
 
136
  event_row = catalog[catalog["event_id"] == selected_event].iloc[0]
137
  event_record = event_row.to_dict()
138
+ event_stages = _sort_stage_frame(stages[stages["event_id"] == selected_event])
139
  summary_row = summary[summary["event_id"] == selected_event]
140
+ event_links = build_event_links(selected_event, str(event_record.get("gantt_html_path") or ""))
141
 
142
+ st.caption(filter_summary_text(len(filtered_rows), len(catalog_rows)))
143
+
144
+ tabs = st.tabs(["Catalog", "Event detail", "Timeline", "Stages", "FinalCascade JSON", "Access and boundary"])
145
 
146
  with tabs[0]:
147
+ st.subheader("Event catalog")
148
  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)
149
 
150
  with tabs[1]:
151
  st.subheader(event_name(event_record))
152
  st.write(event_description(event_record))
153
+ c1, c2, c3, c4, c5 = st.columns(5)
154
+ c1.metric("Sources", _safe_int(event_row.get("source_count", 0)))
155
+ c2.metric("Stages", _safe_int(event_row.get("stage_count", 0)))
156
+ c3.metric("Episodes", _safe_int(event_row.get("episode_count", 0)))
157
+ c4.metric("Participants", _safe_int(event_row.get("participant_count", 0)))
158
+ c5.metric("Relations", _safe_int(event_row.get("relation_count", 0)))
159
+
160
+ st.markdown("#### Event profile")
161
+ profile_columns = [
162
+ "event_id",
163
+ "display_name",
164
+ "domain",
165
+ "event_category",
166
+ "event_scope_label",
167
+ "keywords",
168
+ "event_boundary_time_status",
169
+ "temporal_anchor_summary",
170
+ "gold_reference_access_level",
171
+ "finalcascade_access_level",
172
+ ]
173
+ st.dataframe(_select_columns(catalog[catalog["event_id"] == selected_event], profile_columns), use_container_width=True)
174
+
175
+ link_cols = st.columns(4)
176
+ link_cols[0].link_button("Open dataset", event_links["public_dataset"], use_container_width=True)
177
+ link_cols[1].link_button("Gold access", event_links["gold_request"], use_container_width=True)
178
+ link_cols[2].link_button("FinalCascade file", event_links["finalcascade_jsonl"], use_container_width=True)
179
+ if "gantt_html" in event_links:
180
+ link_cols[3].link_button("Gantt artifact", event_links["gantt_html"], use_container_width=True)
181
+
182
  if not summary_row.empty:
183
  st.markdown("#### Public FinalCascade summary")
184
+ summary_columns = [
185
+ "event_id",
186
+ "stage_count",
187
+ "episode_count",
188
+ "participant_count",
189
+ "transaction_count",
190
+ "relation_count",
191
+ "event_boundary_time_status",
192
+ "known_action_time_anchor_count",
193
+ "not_gold_warning",
194
+ ]
195
+ st.dataframe(_select_columns(summary_row, summary_columns), use_container_width=True)
196
 
197
  with tabs[2]:
198
  figure = build_timeline_figure(_as_records(event_stages), selected_event)
src/h2epr_explorer/constants.py CHANGED
@@ -17,13 +17,14 @@ RELEASE_BOUNDARY_NOTICE = (
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
-
 
17
 
18
  CATALOG_COLUMNS = [
19
  "event_id",
20
+ "display_name",
21
  "domain",
22
  "event_category",
23
+ "event_descriptor_en",
24
  "keywords",
25
  "source_count",
26
  "stage_count",
27
+ "event_boundary_time_status",
28
+ "known_action_time_anchor_count",
29
  "gantt_html_path",
30
  ]
 
src/h2epr_explorer/navigation.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from .constants import GOLD_COMPANION_REPO, PUBLIC_DATASET_REPO
6
+
7
+
8
+ SPACE_URL = "https://huggingface.co/spaces/AgenticFinLab/H2EPR-Bench-Explorer"
9
+ PUBLIC_DATASET_URL = f"https://huggingface.co/datasets/{PUBLIC_DATASET_REPO}"
10
+ GOLD_COMPANION_URL = f"https://huggingface.co/datasets/{GOLD_COMPANION_REPO}"
11
+
12
+
13
+ def _first_value(value: Any) -> str:
14
+ if isinstance(value, (list, tuple)):
15
+ return str(value[0]).strip() if value else ""
16
+ return str(value or "").strip()
17
+
18
+
19
+ def query_param_event_id(query_params: Mapping[str, Any]) -> str:
20
+ return _first_value(query_params.get("event_id"))
21
+
22
+
23
+ def resolve_selected_event_index(rows: list[dict[str, Any]], requested_event_id: str = "") -> int:
24
+ if not rows:
25
+ return 0
26
+ if requested_event_id:
27
+ for index, row in enumerate(rows):
28
+ if str(row.get("event_id", "")).strip() == requested_event_id:
29
+ return index
30
+ return 0
31
+
32
+
33
+ def build_event_links(event_id: str, gantt_html_path: str = "") -> dict[str, str]:
34
+ event_id = event_id.strip()
35
+ links = {
36
+ "explorer": f"{SPACE_URL}?event_id={event_id}",
37
+ "public_dataset": PUBLIC_DATASET_URL,
38
+ "gold_request": GOLD_COMPANION_URL,
39
+ "finalcascade_jsonl": f"{PUBLIC_DATASET_URL}/blob/main/data/finmycelium_finalcascade_public.jsonl",
40
+ }
41
+ if gantt_html_path:
42
+ links["gantt_html"] = f"{PUBLIC_DATASET_URL}/blob/main/{gantt_html_path.lstrip('/')}"
43
+ return links
44
+
45
+
46
+ def filter_summary_text(filtered_count: int, total_count: int) -> str:
47
+ return f"Showing {filtered_count:,} of {total_count:,} events"
src/h2epr_explorer/render_gantt.py CHANGED
@@ -7,18 +7,42 @@ 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
 
@@ -29,6 +53,8 @@ def prepare_gantt_rows(stage_rows: list[dict[str, Any]]) -> list[dict[str, Any]]
29
  prepared.append(
30
  {
31
  **row,
 
 
32
  "display_start": display_start,
33
  "display_end": display_end,
34
  "axis_mode": axis_mode,
@@ -52,8 +78,8 @@ def build_timeline_figure(stage_rows: list[dict[str, Any]], event_id: str):
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
  )
@@ -63,14 +89,13 @@ def build_timeline_figure(stage_rows: list[dict[str, Any]], event_id: str):
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
-
 
7
  return bool(value) and str(value).strip().lower() not in {"unknown", "none", "nan", "nat"}
8
 
9
 
10
+ def _stage_order(row: dict[str, Any], fallback_index: int = 0) -> int:
11
+ value = row.get("stage_index", row.get("stage_order", fallback_index))
12
+ try:
13
+ return int(value)
14
+ except (TypeError, ValueError):
15
+ return fallback_index
16
+
17
+
18
+ def _stage_label(row: dict[str, Any]) -> str:
19
+ for field in ("stage_title", "stage_label_public", "stage_label", "stage_id"):
20
+ value = str(row.get(field) or "").strip()
21
+ if value:
22
+ return value
23
+ return "Unnamed stage"
24
+
25
+
26
  def prepare_gantt_rows(stage_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
27
+ ordered = sorted(
28
+ stage_rows,
29
+ key=lambda row: (_stage_order(row), str(row.get("stage_id", ""))),
30
+ )
31
+ calendar_axis = all(
32
+ _is_known_time(row.get("stage_start_time")) and _is_known_time(row.get("stage_end_time"))
33
+ for row in ordered
34
+ )
35
  prepared: list[dict[str, Any]] = []
36
  for fallback_index, row in enumerate(ordered, start=1):
37
  start = row.get("stage_start_time")
38
  end = row.get("stage_end_time")
39
+ stage_order = _stage_order(row, fallback_index)
40
+ if calendar_axis:
41
  display_start = start
42
  display_end = end
43
  axis_mode = "calendar"
44
  else:
45
+ display_start = stage_order
46
  display_end = display_start + 0.85
47
  axis_mode = "relative_order"
48
 
 
53
  prepared.append(
54
  {
55
  **row,
56
+ "stage_label": _stage_label(row),
57
+ "stage_order": stage_order,
58
  "display_start": display_start,
59
  "display_end": display_end,
60
  "axis_mode": axis_mode,
 
78
  frame,
79
  x_start="display_start",
80
  x_end="display_end",
81
+ y="stage_label",
82
+ color="stage_label",
83
  hover_data=["stage_id", "stage_order", "time_note"],
84
  title=f"{event_id}: public stage timeline",
85
  )
 
89
  fig = px.bar(
90
  frame,
91
  x=[row["display_end"] - row["display_start"] for row in prepared],
92
+ y="stage_label",
93
  base="display_start",
94
  orientation="h",
95
+ color="stage_label",
96
  hover_data=["stage_id", "stage_order", "time_note"],
97
  title=f"{event_id}: relative stage order",
98
  )
99
  fig.update_yaxes(autorange="reversed")
100
  fig.update_layout(xaxis_title="Relative stage order")
101
  return fig