cjc0013 commited on
Commit
f1d0827
·
verified ·
1 Parent(s): a7d08d8

Simplify graph UX with focused default member view

Browse files
dataset_bundle/evidence_audit/consistency_report.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "generated_at": "2026-04-19T10:01:10-04:00",
3
  "event_provenance": {
4
  "event_count": 3918,
5
  "events_with_artifacts": 3878,
 
1
  {
2
+ "generated_at": "2026-04-19T11:09:07-04:00",
3
  "event_provenance": {
4
  "event_count": 3918,
5
  "events_with_artifacts": 3878,
dataset_bundle/network_graph/graph_config.json CHANGED
@@ -20,9 +20,10 @@
20
  "default_filters": {
21
  "relationship_family": "sector",
22
  "review_status": "stronger",
23
- "max_edges": 60,
24
  "hide_unresolved_only": true,
25
- "overview_member_limit": 8
 
26
  },
27
  "example_member_searches": [
28
  "Josh Gottheimer",
 
20
  "default_filters": {
21
  "relationship_family": "sector",
22
  "review_status": "stronger",
23
+ "max_edges": 30,
24
  "hide_unresolved_only": true,
25
+ "overview_member_limit": 5,
26
+ "default_member_search": "Josh Gottheimer"
27
  },
28
  "example_member_searches": [
29
  "Josh Gottheimer",
dataset_bundle/public_release_manifest.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "public_version": "congress-public-records-slice-2026-04-v1",
3
  "title": "Congress Public Records Slice",
4
- "release_date": "2026-04-19T10:02:16-04:00",
5
  "slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
6
  "source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
7
  "dataset_repo_id": "cjc0013/cmp-data",
 
1
  {
2
  "public_version": "congress-public-records-slice-2026-04-v1",
3
  "title": "Congress Public Records Slice",
4
+ "release_date": "2026-04-19T08:23:16-04:00",
5
  "slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
6
  "source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
7
  "dataset_repo_id": "cjc0013/cmp-data",
public_space_app.py CHANGED
@@ -153,15 +153,27 @@ def _graph_intro_markdown(config: Dict[str, Any]) -> str:
153
  status_counts = config.get("relationship_status_counts") or {}
154
  defaults = config.get("default_filters") or {}
155
  example_members = [str(item) for item in (config.get("example_member_searches") or []) if str(item).strip()]
 
 
 
 
 
 
 
 
 
 
 
156
  return "\n".join(
157
  [
158
  "### What you are looking at",
159
  "",
160
  "- Green dots are House members, rust dots are funding recipients, and gold dots are sectors.",
161
  "- Thicker lines mean more supporting relationship rows in this released slice.",
162
- f"- This graph opens in a simpler `{_plain_family_label(str(defaults.get('relationship_family', 'sector'))).lower()}` overview so the first screen is easier to read.",
163
  f"- The default status filter is `{_plain_status_label(str(defaults.get('review_status', 'stronger'))).lower()}`.",
164
  f"- Unresolved-only edges start hidden: `{str(bool(defaults.get('hide_unresolved_only', True))).lower()}`.",
 
165
  *([f"- Example member searches: {', '.join(f'`{item}`' for item in example_members)}."] if example_members else []),
166
  f"- Current graph inventory: `{int(node_counts.get('member', 0) or 0)}` members, `{int(node_counts.get('recipient', 0) or 0)}` recipients, `{int(node_counts.get('sector', 0) or 0)}` sectors.",
167
  f"- Relationship counts: `{int(edge_counts.get('recipient', 0) or 0)}` recipient edges, `{int(edge_counts.get('sector', 0) or 0)}` sector edges.",
@@ -172,6 +184,84 @@ def _graph_intro_markdown(config: Dict[str, Any]) -> str:
172
  )
173
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  def _filter_events(events: pd.DataFrame, member_query: str, event_type: str, score_label: str, text_query: str) -> pd.DataFrame:
176
  filtered = events.copy()
177
  if member_query.strip():
@@ -261,11 +351,28 @@ def _render_graph(nodes: pd.DataFrame, edges: pd.DataFrame) -> str:
261
  if edges.empty:
262
  return "<div style=\"padding: 1rem; border: 1px solid #d6d0c4; background: #fffdf8; color: #3a3a3a;\">No relationships match the current filters.</div>"
263
  network = Network(height="720px", width="100%", bgcolor="#fbf7ee", font_color="#1f2b2d")
264
- network.barnes_hut(gravity=-15000, central_gravity=0.15, spring_length=220, spring_strength=0.02)
265
  network.set_options("""
266
  var options = {
267
  "interaction": {"hover": true, "tooltipDelay": 120, "navigationButtons": true, "keyboard": true},
268
- "physics": {"stabilization": {"enabled": true, "iterations": 250}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
  """)
271
  color_map = {"member": "#1f5f5b", "recipient": "#a24e2c", "sector": "#c08d2e"}
@@ -301,6 +408,7 @@ def _render_graph(nodes: pd.DataFrame, edges: pd.DataFrame) -> str:
301
  title="<br>".join(title_lines),
302
  color=color_map.get(str(node.get("node_type", "")), "#6e6e6e"),
303
  shape="dot",
 
304
  size=16 + min(int(node.get("connected_edge_count", 0) or 0), 20),
305
  )
306
  for row in edges.to_dict("records"):
@@ -428,6 +536,7 @@ def build_app(copy_path: str | Path):
428
  ("Funding recipients", "recipient"),
429
  ("All relationships", "all"),
430
  ]
 
431
  event_id_choices = sorted(events["event_id"].dropna().unique().tolist())
432
  graph_defaults = data["graph_config"].get("default_filters") or {}
433
  overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
@@ -438,22 +547,34 @@ def build_app(copy_path: str | Path):
438
  gr.Markdown(_graph_intro_markdown(data["graph_config"]))
439
  with gr.Row():
440
  family = gr.Dropdown(label="Relationship view", choices=graph_family_choices, value=str(graph_defaults.get("relationship_family", "sector")))
441
- member_graph_query = gr.Textbox(label="Member name or slug")
442
  target_query = gr.Textbox(label="Recipient or sector search")
443
  graph_score = gr.Dropdown(label="Score label", choices=graph_score_choices, value="all")
444
  review_status = gr.Dropdown(label="Relationship strength", choices=graph_status_choices, value=str(graph_defaults.get("review_status", "stronger")))
 
 
445
  with gr.Row():
446
  hide_unresolved_only = gr.Checkbox(label="Hide unresolved relationships", value=bool(graph_defaults.get("hide_unresolved_only", True)))
447
  max_edges = gr.Slider(label="Max visible relationships", minimum=25, maximum=300, step=25, value=int(graph_defaults.get("max_edges", 60)))
 
448
  graph_html = gr.HTML()
 
449
  graph_df = gr.Dataframe(interactive=False)
450
  def _update_graph(family: str, member_graph_query: str, target_query: str, graph_score: str, review_status: str, hide_unresolved_only: bool, max_edges: int):
451
  filtered_edges = _filter_graph(edges, family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges, overview_member_limit)
452
  filtered_nodes = nodes[nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))]
453
- return _render_graph(filtered_nodes, filtered_edges), filtered_edges
 
 
 
 
 
 
 
 
454
  for control in (family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges):
455
- control.change(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_html, graph_df])
456
- app.load(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_html, graph_df])
457
  with gr.Tab("Explore"):
458
  with gr.Row():
459
  member_query = gr.Textbox(label="Member name or slug")
 
153
  status_counts = config.get("relationship_status_counts") or {}
154
  defaults = config.get("default_filters") or {}
155
  example_members = [str(item) for item in (config.get("example_member_searches") or []) if str(item).strip()]
156
+ default_member = str(defaults.get("default_member_search", "") or "").strip()
157
+ opening_line = (
158
+ f"- This graph opens focused on `{default_member}` so the first view is readable."
159
+ if default_member
160
+ else f"- This graph opens in a small `{_plain_family_label(str(defaults.get('relationship_family', 'sector'))).lower()}` overview."
161
+ )
162
+ next_step_line = (
163
+ "- Replace the member name above to explore someone else, or clear it to return to the small overview."
164
+ if default_member
165
+ else "- Search one House member above for the clearest view."
166
+ )
167
  return "\n".join(
168
  [
169
  "### What you are looking at",
170
  "",
171
  "- Green dots are House members, rust dots are funding recipients, and gold dots are sectors.",
172
  "- Thicker lines mean more supporting relationship rows in this released slice.",
173
+ opening_line,
174
  f"- The default status filter is `{_plain_status_label(str(defaults.get('review_status', 'stronger'))).lower()}`.",
175
  f"- Unresolved-only edges start hidden: `{str(bool(defaults.get('hide_unresolved_only', True))).lower()}`.",
176
+ next_step_line,
177
  *([f"- Example member searches: {', '.join(f'`{item}`' for item in example_members)}."] if example_members else []),
178
  f"- Current graph inventory: `{int(node_counts.get('member', 0) or 0)}` members, `{int(node_counts.get('recipient', 0) or 0)}` recipients, `{int(node_counts.get('sector', 0) or 0)}` sectors.",
179
  f"- Relationship counts: `{int(edge_counts.get('recipient', 0) or 0)}` recipient edges, `{int(edge_counts.get('sector', 0) or 0)}` sector edges.",
 
184
  )
185
 
186
 
187
+ def _graph_view_summary_markdown(
188
+ edges: pd.DataFrame,
189
+ *,
190
+ family: str,
191
+ member_query: str,
192
+ target_query: str,
193
+ review_status: str,
194
+ max_edges: int,
195
+ ) -> str:
196
+ if edges.empty:
197
+ return "\n".join(
198
+ [
199
+ "### Current view",
200
+ "",
201
+ "No relationships match the current filters.",
202
+ "",
203
+ "Try one House member name, switch relationship view, or clear the current filters.",
204
+ ]
205
+ )
206
+ member_count = int(edges["member_slug"].nunique())
207
+ target_count = int(edges["target_key"].nunique())
208
+ visible_count = int(len(edges))
209
+ family_label = _plain_family_label(family)
210
+ status_label = _plain_status_label(review_status)
211
+ lines = [
212
+ "### Current view",
213
+ "",
214
+ f"- Showing `{visible_count}` visible relationships across `{member_count}` House members and `{target_count}` targets.",
215
+ f"- Relationship view: `{family_label}`",
216
+ f"- Strength filter: `{status_label}`",
217
+ f"- Visible relationship cap: `{int(max_edges)}`",
218
+ ]
219
+ if member_query.strip():
220
+ focus_members = ", ".join(sorted({str(value) for value in edges["member_name"].fillna("").tolist() if str(value).strip()})[:4])
221
+ if focus_members:
222
+ lines.append(f"- Focused on: `{focus_members}`")
223
+ lines.append("- Tip: change the member name above to compare someone else, or clear it to return to the small overview.")
224
+ else:
225
+ lines.append("- This is an overview, so it only shows a small set of members. Search one member name for the clearest read.")
226
+ if target_query.strip():
227
+ lines.append(f"- Target filter: `{target_query.strip()}`")
228
+ return "\n".join(lines)
229
+
230
+
231
+ def _graph_table(edges: pd.DataFrame) -> pd.DataFrame:
232
+ if edges.empty:
233
+ return pd.DataFrame(columns=["member", "target", "relationship_view", "strength", "supporting_rows"])
234
+ rows: list[dict[str, Any]] = []
235
+ for row in edges.to_dict("records"):
236
+ status = str(row.get("relationship_status", "") or "")
237
+ family = str(row.get("relationship_family", "") or "")
238
+ stronger_support = int(
239
+ row.get("linked_count", 0) or 0
240
+ if family == "recipient"
241
+ else row.get("strong_event_count", 0) or 0
242
+ )
243
+ caution_support = int(
244
+ row.get("review_count", 0) or 0
245
+ if family == "recipient"
246
+ else row.get("weak_event_count", 0) or 0
247
+ )
248
+ source_examples = ", ".join(_split_pipe_values(row.get("source_urls", ""), limit=2))
249
+ rows.append(
250
+ {
251
+ "member": str(row.get("member_name") or row.get("member_slug") or ""),
252
+ "target": str(row.get("target_label") or ""),
253
+ "relationship_view": _plain_family_label(family),
254
+ "strength": _plain_status_label(status),
255
+ "supporting_rows": int(row.get("link_count", 0) or 0),
256
+ "stronger_support": stronger_support,
257
+ "caution_support": caution_support,
258
+ "unresolved_refs": int(row.get("unresolved_source_ref_count", 0) or 0),
259
+ "source_examples": source_examples,
260
+ }
261
+ )
262
+ return pd.DataFrame(rows)
263
+
264
+
265
  def _filter_events(events: pd.DataFrame, member_query: str, event_type: str, score_label: str, text_query: str) -> pd.DataFrame:
266
  filtered = events.copy()
267
  if member_query.strip():
 
351
  if edges.empty:
352
  return "<div style=\"padding: 1rem; border: 1px solid #d6d0c4; background: #fffdf8; color: #3a3a3a;\">No relationships match the current filters.</div>"
353
  network = Network(height="720px", width="100%", bgcolor="#fbf7ee", font_color="#1f2b2d")
 
354
  network.set_options("""
355
  var options = {
356
  "interaction": {"hover": true, "tooltipDelay": 120, "navigationButtons": true, "keyboard": true},
357
+ "physics": false,
358
+ "layout": {
359
+ "hierarchical": {
360
+ "enabled": true,
361
+ "direction": "LR",
362
+ "sortMethod": "directed",
363
+ "nodeSpacing": 170,
364
+ "treeSpacing": 220,
365
+ "levelSeparation": 220
366
+ }
367
+ },
368
+ "edges": {
369
+ "smooth": {
370
+ "enabled": true,
371
+ "type": "cubicBezier",
372
+ "forceDirection": "horizontal",
373
+ "roundness": 0.35
374
+ }
375
+ }
376
  }
377
  """)
378
  color_map = {"member": "#1f5f5b", "recipient": "#a24e2c", "sector": "#c08d2e"}
 
408
  title="<br>".join(title_lines),
409
  color=color_map.get(str(node.get("node_type", "")), "#6e6e6e"),
410
  shape="dot",
411
+ level=0 if node_type == "member" else 1,
412
  size=16 + min(int(node.get("connected_edge_count", 0) or 0), 20),
413
  )
414
  for row in edges.to_dict("records"):
 
536
  ("Funding recipients", "recipient"),
537
  ("All relationships", "all"),
538
  ]
539
+ example_member_choices = [[item] for item in data["graph_config"].get("example_member_searches") or []]
540
  event_id_choices = sorted(events["event_id"].dropna().unique().tolist())
541
  graph_defaults = data["graph_config"].get("default_filters") or {}
542
  overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
 
547
  gr.Markdown(_graph_intro_markdown(data["graph_config"]))
548
  with gr.Row():
549
  family = gr.Dropdown(label="Relationship view", choices=graph_family_choices, value=str(graph_defaults.get("relationship_family", "sector")))
550
+ member_graph_query = gr.Textbox(label="House member to focus", value=str(graph_defaults.get("default_member_search", "")))
551
  target_query = gr.Textbox(label="Recipient or sector search")
552
  graph_score = gr.Dropdown(label="Score label", choices=graph_score_choices, value="all")
553
  review_status = gr.Dropdown(label="Relationship strength", choices=graph_status_choices, value=str(graph_defaults.get("review_status", "stronger")))
554
+ if example_member_choices:
555
+ gr.Examples(examples=example_member_choices, inputs=[member_graph_query], label="Try one of these example members")
556
  with gr.Row():
557
  hide_unresolved_only = gr.Checkbox(label="Hide unresolved relationships", value=bool(graph_defaults.get("hide_unresolved_only", True)))
558
  max_edges = gr.Slider(label="Max visible relationships", minimum=25, maximum=300, step=25, value=int(graph_defaults.get("max_edges", 60)))
559
+ graph_summary_md = gr.Markdown()
560
  graph_html = gr.HTML()
561
+ gr.Markdown("#### Relationships in this view")
562
  graph_df = gr.Dataframe(interactive=False)
563
  def _update_graph(family: str, member_graph_query: str, target_query: str, graph_score: str, review_status: str, hide_unresolved_only: bool, max_edges: int):
564
  filtered_edges = _filter_graph(edges, family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges, overview_member_limit)
565
  filtered_nodes = nodes[nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))]
566
+ summary = _graph_view_summary_markdown(
567
+ filtered_edges,
568
+ family=family,
569
+ member_query=member_graph_query,
570
+ target_query=target_query,
571
+ review_status=review_status,
572
+ max_edges=max_edges,
573
+ )
574
+ return summary, _render_graph(filtered_nodes, filtered_edges), _graph_table(filtered_edges)
575
  for control in (family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges):
576
+ control.change(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_summary_md, graph_html, graph_df])
577
+ app.load(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_summary_md, graph_html, graph_df])
578
  with gr.Tab("Explore"):
579
  with gr.Row():
580
  member_query = gr.Textbox(label="Member name or slug")