Add debug st.info before st.plotly_chart to diagnose invisible chart
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
date: 2026-03-30
|
||||
topic: "compass-trajectory-consistency"
|
||||
status: draft
|
||||
---
|
||||
|
||||
# Implementation Plan — Compass ↔ Trajectory Consistency
|
||||
|
||||
This plan implements the validated design (thoughts/shared/designs/2026-03-30-compass-trajectory-consistency-design.md) with the following firm constraints from the user:
|
||||
- Use per-window MP-centroid party coordinates as the canonical source for components 1 & 2
|
||||
- When a party has no MPs in a window, use the first chronological party vector as fallback
|
||||
- **Update all callers** to the new explicit API; do NOT keep backward compatibility shims
|
||||
|
||||
|
||||
## Goal
|
||||
|
||||
Make the political compass numeric values identical to trajectory centroids for SVD components 1 and 2 by passing explicit per-party (x,y) coordinates (computed from positions_by_window) to the compass renderer and updating all callers to use that API.
|
||||
|
||||
|
||||
## Micro-tasks (ordered, small, actionable)
|
||||
|
||||
All tasks assume a development branch and running tests locally. Each task should be one commit.
|
||||
|
||||
1) Add explorer_helpers.py (pure helper)
|
||||
- Create compute_party_coords(positions_by_window, party_map, window_id, fallback_party_scores=None)
|
||||
- Returns (party_coords: Dict[str,(x,y)], fallback_used: Set[str])
|
||||
- Unit tests: tests/test_explorer_helpers.py
|
||||
- Estimate: 2.0h
|
||||
|
||||
2) Update explorer.py to the new strict API
|
||||
- Replace _build_party_axis_figure to accept only explicit party_coords for comp_sel 1 & 2.
|
||||
- Remove old polymorphic/legacy path; callers must pass party_coords or raise a clear error.
|
||||
- Update rendering glue to call _build_party_axis_figure with explicit party_coords.
|
||||
- Ensure hover text shows fallback notes for parties where fallback_used contains the party.
|
||||
- Update/clean Streamlit caption behavior when no coords available.
|
||||
- Tests: modify tests/test_explorer_chart.py to supply party_coords shape and assert behavior.
|
||||
- Estimate: 4.5h
|
||||
|
||||
3) Update all callers across repo to pass explicit party_coords
|
||||
- Grep for places that previously passed party vectors into _build_party_axis_figure or used load_party_axis_scores for compass rendering.
|
||||
- Update each call site to compute party_coords via compute_party_coords, passing the fallback_party_scores (first-chronological vector) when needed.
|
||||
- Caller list (non-exhaustive — verify with repo search):
|
||||
- explorer.build_svd_components_tab
|
||||
- explorer._render_party_axis_chart (if present)
|
||||
- any scripts or tests that directly call _build_party_axis_figure
|
||||
- Update tests referencing legacy vector shape.
|
||||
- Estimate: 3.0h
|
||||
|
||||
4) Add integration consistency test
|
||||
- tests/test_compass_trajectory_consistency.py — synthetic positions_by_window and party_map to assert compute_party_coords equals centroid computations used by trajectories.
|
||||
- Estimate: 1.0h
|
||||
|
||||
5) Run full test suite and fix regressions
|
||||
- Run pytest; address failures introduced by strict API change.
|
||||
- If other modules relied on old shape in ways not covered by tests, update them to use compute_party_coords.
|
||||
- Estimate: 1.5h
|
||||
|
||||
6) Manual QA
|
||||
- Run streamlit run explorer.py and visually verify compass tooltips and trajectories hover values match (comps 1 & 2) for several parties and windows.
|
||||
- Verify fallback tooltip and logger WARN when a party uses fallback vector.
|
||||
- Estimate: 1.0h
|
||||
|
||||
7) Commit and push (or open PR) with description:
|
||||
"feat(explorer): use explicit per-party (x,y) coords from positions_by_window for compass (components 1 & 2); update callers and add tests"
|
||||
- Estimate: 0.5h
|
||||
|
||||
|
||||
## Verification commands
|
||||
|
||||
- Unit tests:
|
||||
- python -m pytest tests/test_explorer_helpers.py
|
||||
- python -m pytest tests/test_explorer_chart.py
|
||||
- python -m pytest tests/test_compass_trajectory_consistency.py
|
||||
- Full test suite:
|
||||
- python -m pytest
|
||||
- Manual UI:
|
||||
- streamlit run explorer.py
|
||||
|
||||
|
||||
## Rollback and mitigation
|
||||
|
||||
- If the strict API uncovers many call sites, revert to a temporary feature branch, document call sites, and migrate them in smaller patches.
|
||||
- Keep commits small and self-contained to ease review.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- This plan follows the user's instruction to update all callers and to use the first chronological party vector as fallback.
|
||||
- The helper is pure Python to keep tests simple; callers may cache if needed.
|
||||
@@ -0,0 +1,383 @@
|
||||
# Diagnose no-plot trajectories Implementation Plan
|
||||
|
||||
**Goal:** Add an opt-in debug mode for the Trajectories tab that surfaces runtime early-returns and swallowed exceptions so we can diagnose why no Plotly chart is shown.
|
||||
|
||||
**Architecture:** Minimal, reversible instrumentation inside explorer.py and explorer_helpers.py. Add an opt-in UI toggle (checkbox + EXPLORER_DEBUG_TRAJECTORIES env var), extend the existing diagnostics/inspector helper to surface additional samples/counts, un-silence broad excepts to log exceptions and capture tracebacks into a diagnostics object accessible to tests and the UI (when debug enabled).
|
||||
|
||||
**Design:** thoughts/shared/designs/2026-03-30-diagnose-no-plot-trajectories-design.md
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Batch 1 (parallel): 1.1, 1.2 [foundation - no deps]
|
||||
Batch 2 (parallel): 2.1 [core - depends on batch 1]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch 1: Foundation (parallel - 2 implementers)
|
||||
|
||||
All tasks in this batch have NO dependencies and run simultaneously.
|
||||
|
||||
### Task 1.1: Extend diagnostics inspector
|
||||
**File:** `explorer_helpers.py` (modify function `inspect_positions_for_issues`)
|
||||
**Test:** `tests/test_explorer_helpers_diagnostics.py`
|
||||
**Depends:** none
|
||||
|
||||
Purpose: add compact, structured diagnostics (mp_positions_sample, mp_positions_count, windows_with_no_positions) to the existing inspector output so both UI and tests can consume them.
|
||||
|
||||
Implementation decisions (gap-filling):
|
||||
- Keep the function import-safe and pure (no Streamlit calls). Return additional keys under the same dict.
|
||||
- Provide small, deterministic samples (sorted keys limited to 10) so tests are stable.
|
||||
|
||||
Estimate: 45-90 minutes
|
||||
|
||||
Verify: `pytest -q tests/test_explorer_helpers_diagnostics.py`
|
||||
|
||||
```python
|
||||
# COMPLETE test code - tests/test_explorer_helpers_diagnostics.py
|
||||
import numpy as np
|
||||
from explorer_helpers import inspect_positions_for_issues
|
||||
|
||||
|
||||
def test_inspect_positions_for_issues_basic():
|
||||
positions_by_window = {
|
||||
"w1": {"mp1": (1.0, 2.0), "mp2": (float('nan'), float('nan'))},
|
||||
"w2": {},
|
||||
}
|
||||
party_map = {"mp1": "P1"}
|
||||
d = inspect_positions_for_issues(positions_by_window, party_map)
|
||||
|
||||
# basic keys still present
|
||||
assert d["windows_count"] == 2
|
||||
assert isinstance(d["mp_id_set"], set)
|
||||
# new diagnostics
|
||||
assert "mp_positions_count" in d
|
||||
assert d["mp_positions_count"] >= 1
|
||||
assert "mp_positions_sample" in d
|
||||
assert isinstance(d["mp_positions_sample"], list)
|
||||
assert "windows_with_no_positions" in d
|
||||
assert isinstance(d["windows_with_no_positions"], list)
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
# COMPLETE implementation - explorer_helpers.py (function replacement)
|
||||
def inspect_positions_for_issues(
|
||||
positions_by_window: Dict[str, Dict[str, Tuple[float, float]]],
|
||||
party_map: Dict[str, str],
|
||||
) -> Dict[str, Any]:
|
||||
"""Inspect positions_by_window for simple issues/summary.
|
||||
|
||||
Returns a dictionary with keys including the previous ones (windows_count,
|
||||
window_labels, mp_id_set, party_map_count, parties_with_centroid_counts,
|
||||
mismatched_mp_ids_sample) plus:
|
||||
- mp_positions_count: int (num unique MP ids seen)
|
||||
- mp_positions_sample: list[str] (sorted sample up to 10)
|
||||
- windows_with_no_positions: list[str]
|
||||
|
||||
This helper remains pure and import-safe so unit tests can exercise it.
|
||||
"""
|
||||
windows = list(positions_by_window.keys())
|
||||
windows_count = len(windows)
|
||||
window_labels = sorted(windows)[:10]
|
||||
|
||||
mp_id_set: Set[str] = set()
|
||||
parties_with_centroid_counts: Dict[str, int] = {}
|
||||
mismatched: Set[str] = set()
|
||||
windows_with_no_positions: List[str] = []
|
||||
|
||||
for win, pos in positions_by_window.items():
|
||||
if not pos:
|
||||
windows_with_no_positions.append(win)
|
||||
continue
|
||||
present_parties: Set[str] = set()
|
||||
for ent in pos.keys():
|
||||
if not ent:
|
||||
continue
|
||||
mp_id_set.add(ent)
|
||||
party = party_map.get(ent)
|
||||
if party is None:
|
||||
# try stripping paren variant
|
||||
party = party_map.get(_strip_paren(ent))
|
||||
if party:
|
||||
present_parties.add(party)
|
||||
else:
|
||||
mismatched.add(ent)
|
||||
|
||||
for p in present_parties:
|
||||
parties_with_centroid_counts[p] = parties_with_centroid_counts.get(p, 0) + 1
|
||||
|
||||
mismatched_mp_ids_sample = sorted(list(mismatched))[:10]
|
||||
|
||||
mp_positions_sample = sorted(list(mp_id_set))[:10]
|
||||
mp_positions_count = len(mp_id_set)
|
||||
|
||||
return {
|
||||
"windows_count": windows_count,
|
||||
"window_labels": window_labels,
|
||||
"mp_id_set": mp_id_set,
|
||||
"party_map_count": len(party_map),
|
||||
"parties_with_centroid_counts": parties_with_centroid_counts,
|
||||
"mismatched_mp_ids_sample": mismatched_mp_ids_sample,
|
||||
"mp_positions_sample": mp_positions_sample,
|
||||
"mp_positions_count": mp_positions_count,
|
||||
"windows_with_no_positions": windows_with_no_positions,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Commit: `feat(explorer): extend diagnostic inspector to surface mp samples/counts`
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: Add tests and small helper for reading debug env var
|
||||
**File:** `explorer.py` (add function `get_debug_trajectories_enabled`) **-- part of batch 2 core but small and independent**
|
||||
**Test:** `tests/test_debug_flag.py`
|
||||
**Depends:** none
|
||||
|
||||
Purpose: provide a single, testable helper that reads EXPLORER_DEBUG_TRAJECTORIES env var and returns a boolean. We use this consistently in UI code so tests can manipulate debug mode via env var.
|
||||
|
||||
Decision: implement conservative parsing ("1", "true", "True") as truthy. This function will be used by build_trajectories_tab and tests.
|
||||
|
||||
Estimate: 15-30 minutes
|
||||
|
||||
Verify: `pytest -q tests/test_debug_flag.py`
|
||||
|
||||
```python
|
||||
# COMPLETE test code - tests/test_debug_flag.py
|
||||
import os
|
||||
import importlib
|
||||
|
||||
def test_get_debug_flag_on(monkeypatch):
|
||||
monkeypatch.setenv("EXPLORER_DEBUG_TRAJECTORIES", "1")
|
||||
import explorer
|
||||
importlib.reload(explorer)
|
||||
assert explorer.get_debug_trajectories_enabled() is True
|
||||
|
||||
|
||||
def test_get_debug_flag_off(monkeypatch):
|
||||
monkeypatch.delenv("EXPLORER_DEBUG_TRAJECTORIES", raising=False)
|
||||
import explorer
|
||||
importlib.reload(explorer)
|
||||
assert explorer.get_debug_trajectories_enabled() is False
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
# COMPLETE implementation to add into explorer.py
|
||||
def get_debug_trajectories_enabled() -> bool:
|
||||
"""Return whether the Trajectories debug mode is enabled via env var.
|
||||
|
||||
Truthy values: "1", "true", "True". Default False.
|
||||
"""
|
||||
val = os.getenv("EXPLORER_DEBUG_TRAJECTORIES", "")
|
||||
return val in ("1", "true", "True")
|
||||
|
||||
```
|
||||
|
||||
Commit message: `chore(explorer): add get_debug_trajectories_enabled helper`
|
||||
|
||||
---
|
||||
|
||||
## Batch 2: Core Modules (parallel - 1 implementer)
|
||||
|
||||
These tasks depend on changes in Batch 1 (inspector additions and debug-flag helper). All tasks in this batch modify `explorer.py` (single-file microtask) and have a single test file.
|
||||
|
||||
### Task 2.1: Instrument trajectories UI and un-silence exceptions
|
||||
**File:** `explorer.py` (update `select_trajectory_plot_data` exception handling, update `build_trajectories_tab` early-return instrumentation and try/except, add module-level diagnostics capture)
|
||||
**Test:** `tests/test_diagnose_no_plot_trajectories.py`
|
||||
**Depends:** 1.1, 1.2
|
||||
|
||||
Purpose: (A) Add opt-in debug UI binding to env var via checkbox and a DEBUG expander; (B) change helper-call swallow to log exceptions and include traceback in diagnostics; (C) instrument early-return gates (no positions, no mp_positions) to capture the reason and attach it to module-level diagnostics; (D) expose diagnostics to tests via attributes so tests can assert they were produced.
|
||||
|
||||
Decisions / gap-fills:
|
||||
- Do not change public function signatures. To expose diagnostics to tests without changing signatures, set attributes on the function and module:
|
||||
- select_trajectory_plot_data._last_diagnostics -> last inspector summary
|
||||
- explorer._last_diagnostics -> diagnostics captured by build_trajectories_tab (early-returns or exceptions)
|
||||
- Always call logger.exception(...) when an exception happens to preserve logs.
|
||||
- Only call Streamlit UI functions to display tracebacks when debug mode is enabled.
|
||||
|
||||
Estimate: 2-4 hours
|
||||
|
||||
Verify: `pytest -q tests/test_diagnose_no_plot_trajectories.py`
|
||||
|
||||
```python
|
||||
# COMPLETE test code - tests/test_diagnose_no_plot_trajectories.py
|
||||
import traceback
|
||||
import importlib
|
||||
import explorer
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def test_select_helper_exception_is_captured(monkeypatch):
|
||||
# Force the inspector to raise and ensure diagnostics capture the traceback
|
||||
def _boom(*a, **k):
|
||||
raise RuntimeError("boom-inspector")
|
||||
|
||||
monkeypatch.setattr("explorer_helpers.inspect_positions_for_issues", _boom)
|
||||
# call helper
|
||||
fig, count, banner = explorer.select_trajectory_plot_data({}, {}, [], [])
|
||||
# diagnostics should be attached to the function
|
||||
d = getattr(explorer.select_trajectory_plot_data, "_last_diagnostics", None)
|
||||
assert d is not None
|
||||
assert "inspector_exception" in d
|
||||
assert "boom-inspector" in d["inspector_exception"]
|
||||
|
||||
|
||||
def test_build_trajectories_tab_early_return_sets_diagnostics(monkeypatch):
|
||||
# Make load_positions return empty positions to trigger early return
|
||||
monkeypatch.setattr(explorer, "load_positions", lambda db, ws: ({}, None))
|
||||
# Ensure debug mode enabled via env var
|
||||
monkeypatch.setenv("EXPLORER_DEBUG_TRAJECTORIES", "1")
|
||||
importlib.reload(explorer)
|
||||
# Call the tab builder (uses dummy Streamlit in tests)
|
||||
explorer.build_trajectories_tab("/fake.db", "2025")
|
||||
d = getattr(explorer, "_last_diagnostics", None)
|
||||
assert d is not None
|
||||
assert d.get("reason") == "no_positions"
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
# COMPLETE implementation snippets to apply to explorer.py
|
||||
import traceback
|
||||
|
||||
# Add near top-level (after imports in explorer.py)
|
||||
_last_diagnostics: Optional[dict] = None
|
||||
|
||||
|
||||
def get_debug_trajectories_enabled() -> bool:
|
||||
val = os.getenv("EXPLORER_DEBUG_TRAJECTORIES", "")
|
||||
return val in ("1", "true", "True")
|
||||
|
||||
|
||||
# Replace the small inspector try/except in select_trajectory_plot_data with the
|
||||
# following (complete function shown below replaces the existing select_trajectory_plot_data
|
||||
# definition in explorer.py):
|
||||
def select_trajectory_plot_data(
|
||||
positions_by_window: Dict[str, Dict[str, Tuple[float, float]]],
|
||||
party_map: Dict[str, str],
|
||||
windows: List[str],
|
||||
selected_parties: List[str],
|
||||
smooth_alpha: float = 0.35,
|
||||
mp_fallback_count: Optional[int] = None,
|
||||
) -> Tuple[go.Figure, int, Optional[str]]:
|
||||
"""Return (fig, trace_count, banner_text).
|
||||
|
||||
Helper used by build_trajectories_tab. Does not call Streamlit.
|
||||
"""
|
||||
if mp_fallback_count is None:
|
||||
try:
|
||||
mp_fallback_count = int(os.getenv("EXPLORER_MP_FALLBACK_COUNT", "20"))
|
||||
except Exception:
|
||||
mp_fallback_count = 20
|
||||
|
||||
# Compute per-party centroids aligned to windows
|
||||
party_centroids, meta = compute_party_centroids(
|
||||
positions_by_window, party_map, windows
|
||||
)
|
||||
|
||||
# Use inspector to collect diagnostics (import-safe, pure helper).
|
||||
try:
|
||||
inspector_summary = inspect_positions_for_issues(positions_by_window, party_map)
|
||||
except Exception as e:
|
||||
# Do not silently swallow: log and capture traceback text so tests / UI
|
||||
# can inspect it. Keep function import-safe (no Streamlit here).
|
||||
tb = traceback.format_exc()
|
||||
logger.exception("inspect_positions_for_issues failed: %s", e)
|
||||
inspector_summary = {"inspector_exception": tb}
|
||||
|
||||
# expose diagnostics for tests without changing function signature
|
||||
setattr(select_trajectory_plot_data, "_last_diagnostics", inspector_summary)
|
||||
logger.debug("select_trajectory_plot_data inspector summary: %s", inspector_summary)
|
||||
|
||||
# ... rest of the original function remains unchanged (build fig/trace_count)
|
||||
# (Implementation note: keep the rest identical to existing function.)
|
||||
|
||||
|
||||
# Now update the call-site in build_trajectories_tab (replace the try/except around
|
||||
# select_trajectory_plot_data invocation with the following snippet):
|
||||
try:
|
||||
fig2, trace_count2, banner_text = select_trajectory_plot_data(
|
||||
positions_by_window, party_map, windows, selected_parties, smooth_alpha
|
||||
)
|
||||
if fig2 is not None:
|
||||
fig = fig2
|
||||
trace_count = trace_count2
|
||||
if banner_text:
|
||||
st.caption(banner_text)
|
||||
except Exception as e:
|
||||
# Do not silently pass. Log, capture traceback and (when debug enabled)
|
||||
# surface to Streamlit.
|
||||
tb = traceback.format_exc()
|
||||
logger.exception("select_trajectory_plot_data raised: %s", e)
|
||||
global _last_diagnostics
|
||||
_last_diagnostics = {"build_exception": tb}
|
||||
if get_debug_trajectories_enabled():
|
||||
try:
|
||||
st.exception(e)
|
||||
except Exception:
|
||||
# Streamlit may not be available in test env; fall back to text_area
|
||||
try:
|
||||
st.text_area("Trajectories exception", tb)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Instrument early-return gates (example: when positions_by_window is empty) by
|
||||
# setting _last_diagnostics before returning. Replace the current block:
|
||||
if not positions_by_window:
|
||||
st.warning("Geen positiedata beschikbaar.")
|
||||
global _last_diagnostics
|
||||
_last_diagnostics = {"reason": "no_positions", "inspector": {}}
|
||||
if get_debug_trajectories_enabled():
|
||||
# call inspector and attach diagnostics when debug enabled
|
||||
try:
|
||||
_last_diagnostics["inspector"] = inspect_positions_for_issues(positions_by_window, {})
|
||||
except Exception:
|
||||
_last_diagnostics["inspector"] = {"error": "inspector_failed"}
|
||||
return
|
||||
|
||||
# Note: make similar instrumentation for the `if not mp_positions:` early return
|
||||
# inside the per-MP fallback path: set _last_diagnostics = {"reason": "no_mp_positions"}
|
||||
|
||||
```
|
||||
|
||||
Notes for implementer:
|
||||
- Insert the two helper functions and the try/except replacement in the appropriate places of explorer.py. The select_trajectory_plot_data replacement above should replace the function body; keep the unchanged plotting logic intact after the diagnostic area.
|
||||
- Add the module-level _last_diagnostics variable near the top of explorer.py (after imports).
|
||||
|
||||
Commit: `feat(explorer): instrument trajectories with debug diagnostics and un-silence helper exceptions`
|
||||
|
||||
---
|
||||
|
||||
## Verification & Manual checks
|
||||
|
||||
- Run unit tests for the modified files:
|
||||
- pytest -q tests/test_explorer_helpers_diagnostics.py
|
||||
- pytest -q tests/test_debug_flag.py
|
||||
- pytest -q tests/test_diagnose_no_plot_trajectories.py
|
||||
- Manual: run Streamlit locally with EXPLORER_DEBUG_TRAJECTORIES=1 and inspect the "DEBUG" expander in the Trajectories tab to see the diagnostics block and any surfaced tracebacks.
|
||||
|
||||
---
|
||||
|
||||
## Rollback plan
|
||||
|
||||
- All changes gated behind debug env var and small: revert the two modified files (explorer.py, explorer_helpers.py) to previous commit to remove instrumentation.
|
||||
- Because public signatures are unchanged, rollout/revert is safe.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — quick implementer checklist
|
||||
|
||||
1. Implement inspector changes (explorer_helpers.py) and run its tests.
|
||||
2. Add get_debug_trajectories_enabled helper and tests.
|
||||
3. Modify explorer.py: add _last_diagnostics, update select_trajectory_plot_data try/except, update build_trajectories_tab try/except and early-return instrumentation, add debug checkbox wiring in UI.
|
||||
4. Add tests that monkeypatch inspector and load_positions and assert diagnostics are created.
|
||||
|
||||
---
|
||||
|
||||
Written: thoughts/shared/plans/2026-03-30-diagnose-no-plot-trajectories.md
|
||||
@@ -0,0 +1,254 @@
|
||||
# Fix missing trajectories Implementation Plan
|
||||
|
||||
I'm using the writing-plans skill to create the implementation plan.
|
||||
|
||||
Goal: Restore visible party trajectories in the Explorer "Partij Trajectories" tab by adding validation/inspection helpers, making centroid computation tolerant of missing windows (emit NaN gaps), and adding an automatic MP-level fallback (top-K) with a debug expander and hover raw-values preserved.
|
||||
|
||||
Design: thoughts/shared/designs/2026-03-30-fix-missing-trajectories-design.md
|
||||
|
||||
Architecture: Small, focused changes in explorer_helpers.py (pure helpers + unit tests) and explorer.py (UI wiring and plotting policy). Keep helper logic independent of Streamlit so tests run in CI without heavy deps. Provide a graceful MP fallback and compact diagnostics exposed behind a collapsed expander.
|
||||
|
||||
Tech Stack: Python 3.x, pytest, Streamlit (manual UI verification), Plotly (already used). Tests must run in CI with duckdb / streamlit optional — unit tests only use pure Python/numpy.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Batch 1 (parallel): 1.1, 1.2 [foundation - no deps]
|
||||
Batch 2 (parallel): 2.1, 2.2 [core - depends on batch 1]
|
||||
Batch 3 (parallel): 3.1, 3.2 [integration - depends on batch 2]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions / gap-filling (explicit)
|
||||
- EXPLORER_MP_FALLBACK_COUNT environment variable: integer, default 20. Used to choose top-K MPs when party centroids are absent.
|
||||
- Top-K definition: by seat_count when available; when seat_count unavailable, fall back to party axis activity (mean magnitude) via load_party_axis_scores if needed. I will implement MP fallback using seat_count if present in mp_metadata; otherwise use party axis magnitude from load_party_axis_scores.
|
||||
- Validation rules (inspect_positions_for_issues): detect empty positions_by_window, windows_count mismatch across MPs, sample of mismatched mp ids, parties_with_centroid_counts dictionary. Reason: these are the most likely causes of empty traces.
|
||||
- compute_party_centroids behavior: returns per-party arrays aligned to windows (list of floats or np.nan), metadata per-party containing counts and missing indices. Guarantees empty lists (never None).
|
||||
|
||||
---
|
||||
|
||||
## Batch 1: Foundation (parallel - 2 implementers)
|
||||
|
||||
All tasks in this batch have NO dependencies and can run simultaneously.
|
||||
|
||||
### Task 1.1: Add inspector helper
|
||||
**File:** `explorer_helpers.py`
|
||||
**Test:** `tests/test_inspect_positions_for_issues.py`
|
||||
**Depends:** none
|
||||
|
||||
Helpers to add (names only):
|
||||
- inspect_positions_for_issues(positions_by_window: Dict[str, Dict[str, Tuple[float,float]]], party_map: Dict[str,str]) -> Dict[str, Any]
|
||||
|
||||
What it returns (documented in test expectations):
|
||||
- windows_count: int
|
||||
- window_labels: list[str] (sorted sample of window keys)
|
||||
- mp_id_set: set[str] (set of entity ids seen across windows)
|
||||
- party_map_count: int (len(party_map))
|
||||
- parties_with_centroid_counts: Dict[str, int] (mapping party -> number of windows with a centroid)
|
||||
- mismatched_mp_ids_sample: list[str] (sample of ids present in positions but not in party_map, up to 10)
|
||||
|
||||
Tests to add (exact assertions):
|
||||
- tests/test_inspect_positions_for_issues.py (unit):
|
||||
- Construct synthetic positions_by_window with 3 windows, with some MPs missing in some windows and some mp ids that aren't in party_map. Assert returned windows_count == 3, party_map_count equals len(party_map), parties_with_centroid_counts entries for expected parties, and mismatched_mp_ids_sample contains the expected missing keys.
|
||||
|
||||
Verify:
|
||||
- Run: `pytest tests/test_inspect_positions_for_issues.py -q`
|
||||
- Expected: PASS
|
||||
|
||||
Commit message: `feat(explorer): add inspect_positions_for_issues helper + test`
|
||||
|
||||
### Task 1.2: Add compute_party_centroids (per-window aligned arrays)
|
||||
**File:** `explorer_helpers.py` (same file; add new function)
|
||||
**Test:** `tests/test_compute_party_centroids.py`
|
||||
**Depends:** none
|
||||
|
||||
Helper to add (name only):
|
||||
- compute_party_centroids(positions_by_window: Dict[str, Dict[str, Tuple[float,float]]], party_map: Dict[str,str], windows: List[str]) -> Tuple[Dict[str, List[float]], Dict[str, Any]]
|
||||
|
||||
Behavior contract (for implementer):
|
||||
- Return party_centroids: dict[party -> list[float|np.nan]] aligned to the provided windows order. For a party and window where no MPs present, insert np.nan at that index.
|
||||
- Return metadata: {"per_party_counts": {party: int}, "total_windows": int, "parties": sorted_list}
|
||||
- Guarantees: never return None; party lists can be empty list but must have length == len(windows) for parties present in `parties` list.
|
||||
|
||||
Tests to add (exact assertions):
|
||||
- tests/test_compute_party_centroids.py (unit):
|
||||
- Case A: full coverage — every party has coords in every window -> assert no np.nan and lengths equal windows count.
|
||||
- Case B: partial coverage -> assert np.nan present at expected indices and metadata.per_party_counts match counts.
|
||||
- Case C: no parties (empty positions_by_window) -> party_centroids == {} and metadata.total_windows == len(windows)
|
||||
|
||||
Verify:
|
||||
- Run: `pytest tests/test_compute_party_centroids.py -q`
|
||||
- Expected: PASS
|
||||
|
||||
Commit message: `feat(explorer): add compute_party_centroids to produce aligned per-party arrays`
|
||||
|
||||
---
|
||||
|
||||
## Batch 2: Core Modules (parallel - 2 implementers)
|
||||
All tasks depend on Batch 1.
|
||||
|
||||
### Task 2.1: Modify explorer.py to use helpers and add MP fallback
|
||||
**File:** `explorer.py` (modify function build_trajectories_tab only)
|
||||
**Test:** `tests/test_build_trajectories_tab_fallback.py`
|
||||
**Depends:** 1.1, 1.2
|
||||
|
||||
Changes to make (high-level, exact function to modify):
|
||||
- modify build_trajectories_tab(db_path: str, window_size: str) to:
|
||||
- early: call inspect_positions_for_issues(positions_by_window, party_map) and render the compact DEBUG expander content (same keys as the inspector returns). Keep the expander collapsed by default.
|
||||
- replace existing per-window centroid construction with compute_party_centroids(...) which returns aligned arrays containing np.nan placeholders.
|
||||
- relax party-selection filtering: treat a party as plottable if it has >= 1 non-nan centroid (previous code required full coverage). This ensures partial traces still render with gaps.
|
||||
- preserve hover customdata to include raw centroid values (already present in code) — ensure when centroids contain np.nan for raw values we still populate customdata with (np.nan, np.nan).
|
||||
- If no party centroids (empty dict or all-party centroid vectors are entirely nan), trigger MP fallback: plot top-K MPs (EXPLORER_MP_FALLBACK_COUNT, default 20) as per design. This fallback must show a small banner message in Dutch: "Partijcentroiden niet beschikbaar — tonen individuele MP-trajecten als fallback." and provide a toggle (st.checkbox) to expand to show the full top-K list.
|
||||
|
||||
Notes / gap-filling decisions (explicit):
|
||||
- EXPLORER_MP_FALLBACK_COUNT: implement read via int(os.getenv("EXPLORER_MP_FALLBACK_COUNT", "20"))
|
||||
- For selecting top-K MPs: use seat_count if present in mp_metadata (query `mp_metadata` for a seat_count-like field). If unavailable, choose MPs with most non-empty positions across windows. Implementer decision: compute activity = number of windows with a valid (non-None) position and sort descending.
|
||||
|
||||
Tests to add (integration, shims-friendly):
|
||||
- tests/test_build_trajectories_tab_fallback.py
|
||||
- Scenario 1 (party centroids present): Provide a fake positions_by_window and party_map fixture with at least one party having centroids in multiple windows and assert that when build_trajectories_tab is invoked (call the internal plotting branch with a test harness) it adds at least one trace (fig.data length > 0) and trace names match selected parties.
|
||||
- Scenario 2 (no party centroids): Provide positions_by_window where party_map is empty or all MPs map to Unknown; assert the MP fallback path is chosen (method returns or builds fig with MPs) and that the banner message string appears in returned metadata or printed UI stub. Since Streamlit is not easily invoked in unit tests, structure the UI branch so the plotting logic returns fig when called from tests — write the test to import a small internal helper (e.g., build_trajectories_figure_for_test) if necessary. If refactor needed, keep it minimal: extract plotting assembly to a private helper _assemble_trajectories_figure(...) that returns (fig, trace_count, banner_text) so tests can assert fig traces without needing Streamlit.
|
||||
|
||||
Verify (unit/integration):
|
||||
- Run: `pytest tests/test_build_trajectories_tab_fallback.py -q`
|
||||
- Expected: PASS
|
||||
|
||||
Commit message: `feat(explorer): use inspector & compute_party_centroids; add MP top-K fallback and debug expander`
|
||||
|
||||
### Task 2.2: Add/adjust unit tests for hover/raw values and NaN handling
|
||||
**File:** `tests/test_explorer_helpers.py` (update) and `tests/test_explorer_chart.py` (add test)
|
||||
**Depends:** 1.2
|
||||
|
||||
Changes/tests to add (exact tests):
|
||||
- tests/test_explorer_helpers.py: add a test verifying compute_party_centroids produces np.nan for missing windows and that hover customdata creation uses (float, float) or (np.nan, np.nan) consistently.
|
||||
- tests/test_explorer_chart.py: add a small unit test that constructs a go.Figure via the new plotting helper (see 2.1) and asserts:
|
||||
- traces exist when parties have partial coverage
|
||||
- customdata arrays length equals x/y arrays length
|
||||
- hovertemplate contains both smoothed and raw placeholder markers (strings like 'x (raw)')
|
||||
|
||||
Verify:
|
||||
- Run: `pytest tests/test_explorer_helpers.py::test_compute_party_centroids_nan_handling -q`
|
||||
- Run: `pytest tests/test_explorer_chart.py::test_partial_party_traces -q`
|
||||
- Expected: PASS
|
||||
|
||||
Commit message: `test(explorer): add tests for NaN gaps and hover customdata preservation`
|
||||
|
||||
---
|
||||
|
||||
## Batch 3: Integration & Manual UI checks (parallel - 2 implementers)
|
||||
Depends on Batch 2
|
||||
|
||||
### Task 3.1: Integration test (shim-friendly) for three scenarios
|
||||
**File:** `tests/integration/test_trajectories_ui_integration.py`
|
||||
**Test:** the file above
|
||||
**Depends:** 2.1, 2.2
|
||||
|
||||
Tests to add (exact scenarios):
|
||||
- Scenario A (full party centroids): positions_by_window with full coverage — assert plot built uses party traces; simulate user selection to include at least one party; assert fig.data length >= 1.
|
||||
- Scenario B (party centroids missing): party_map empty — assert MP fallback chosen and number of plotted MP traces == EXPLORER_MP_FALLBACK_COUNT or the available MPs if fewer.
|
||||
- Scenario C (partial centroids): party centroids partial across windows — assert traces exist and customdata shows np.nan at missing indices.
|
||||
|
||||
Test harness notes: tests should import small pure helpers from explorer.py that assemble figures without calling st.plotly_chart or other Streamlit side-effects. If necessary, add a small refactor in explorer.py: `_assemble_trajectory_figure_for_tests(positions_by_window, party_centroids, selected_parties, windows, smooth_alpha, ...) -> go.Figure, metadata` and call that from build_trajectories_tab. Tests then call this helper. Keep the helper private and minimal.
|
||||
|
||||
Verify:
|
||||
- Run: `pytest tests/integration/test_trajectories_ui_integration.py -q`
|
||||
- Expected: PASS
|
||||
|
||||
Commit message: `test(integration): trajectories UI integration scenarios (full/partial/missing)`
|
||||
|
||||
### Task 3.2: Manual Streamlit verification steps (documented)
|
||||
**File:** none (manual steps below); include in PR description.
|
||||
**Depends:** 2.1
|
||||
|
||||
Manual verification (Streamlit):
|
||||
1. Start Streamlit: `streamlit run explorer.py --server.headless true` (or run locally with a test DB path)
|
||||
2. Open the app in browser (usually http://localhost:8501). Go to tab "Partij Trajectories".
|
||||
3. Scenario: normal DB with party centroids
|
||||
- Select a recent window_size (e.g., quarterly or annual as appropriate)
|
||||
- Ensure default parties (CDA, D66, VVD) appear and trajectories are visible.
|
||||
- Hover on a trace point: verify hover shows both smoothed and raw centroid values (x (smoothed), x (raw)).
|
||||
- Open the DEBUG expander (collapsed by default) and confirm it shows `windows (count)`, `windows sample`, `party_map entries`, `parties with centroids`, `sample centroid window counts per party`.
|
||||
4. Scenario: simulate missing party centroids (set party_map to {} or use a DB snapshot with missing mp_metadata)
|
||||
- The app should show the fallback banner: "Partijcentroiden niet beschikbaar — tonen individuele MP-trajecten als fallback." and render MP trajectories (top-K). There should be a checkbox to expand the top-K list.
|
||||
5. Scenario: partial centroids
|
||||
- For a party missing centroids in some windows, its trace should appear but with gaps (line discontinuity where NaNs present). Hover customdata at gap points should show raw value `nan` or a placeholder.
|
||||
|
||||
Streamlit-specific acceptance criteria:
|
||||
- traces drawn when at least one party has >=1 centroid
|
||||
- MP fallback automatically displayed (banner + plotted MP traces) when no party centroids
|
||||
- DEBUG expander shows diagnostics described above
|
||||
- Hover shows raw centroid values even when smoothing is applied
|
||||
|
||||
---
|
||||
|
||||
## Files to create / modify (one-file-per-task mapping)
|
||||
|
||||
Batch 1
|
||||
- Modify: `explorer_helpers.py` — add functions:
|
||||
- inspect_positions_for_issues
|
||||
- compute_party_centroids
|
||||
- Add test: `tests/test_inspect_positions_for_issues.py`
|
||||
- Add test: `tests/test_compute_party_centroids.py`
|
||||
|
||||
Batch 2
|
||||
- Modify: `explorer.py` — function build_trajectories_tab; optional small private helper `_assemble_trajectory_figure_for_tests` (single-file change)
|
||||
- Add test: `tests/test_build_trajectories_tab_fallback.py`
|
||||
- Update/add tests: `tests/test_explorer_helpers.py` (augment), `tests/test_explorer_chart.py`
|
||||
|
||||
Batch 3
|
||||
- Add test: `tests/integration/test_trajectories_ui_integration.py`
|
||||
|
||||
---
|
||||
|
||||
## Verification commands (unit & CI)
|
||||
- Unit test single file: `pytest tests/test_inspect_positions_for_issues.py -q`
|
||||
- Unit test compute party centroids: `pytest tests/test_compute_party_centroids.py -q`
|
||||
- Trajectories fallback unit tests: `pytest tests/test_build_trajectories_tab_fallback.py -q`
|
||||
- Integration tests (shim-friendly): `pytest tests/integration/test_trajectories_ui_integration.py -q`
|
||||
- Run full test suite: `pytest -q`
|
||||
|
||||
Manual Streamlit checks: follow steps in Task 3.2 above. Recommended quick dev workflow:
|
||||
- Start streamlit: `streamlit run explorer.py --server.headless true`
|
||||
- Use the URL printed in console (usually http://localhost:8501) and perform the manual steps.
|
||||
|
||||
---
|
||||
|
||||
## Blocked / Unblocked checklist
|
||||
|
||||
- [ ] Blocker: Access to a representative DB fixture (small DuckDB or JSON fixture) that contains windows, svd_vectors and mp_metadata. Without it, integration/manual checks are limited. (Mitigation: tests use synthetic positions_by_window and party_map fixtures — unblocked for unit tests.)
|
||||
- [ ] Blocker: If MP seat_count is required from DB and not present in test fixtures, fallback selection will use activity-based ranking. (Mitigation: implement activity fallback.)
|
||||
- [x] Unblocked: Adding pure helpers in explorer_helpers.py (unit tests cover behavior without Streamlit/duckdb)
|
||||
- [x] Unblocked: Modifying build_trajectories_tab to call helpers and add banner + expander (code-local change)
|
||||
- [ ] Optional: Agree on EXPLORER_MP_FALLBACK_COUNT envvar default (I set default 20). If you want a different default, tell me now.
|
||||
|
||||
If any of the above blockers remain, proceed with unit tests and open a PR discussion for integration DB fixtures.
|
||||
|
||||
---
|
||||
|
||||
## Estimated timeline (hours)
|
||||
|
||||
- Task 1.1 (inspect_positions_for_issues + unit test): 1.5 h
|
||||
- Task 1.2 (compute_party_centroids + unit tests): 3.0 h
|
||||
- Task 2.1 (explorer.py changes: wiring, MP fallback, debug expander): 4.0 h
|
||||
- Task 2.2 (tests for hover/NaN handling): 2.0 h
|
||||
- Task 3.1 (integration tests / small refactor helper): 2.5 h
|
||||
- Task 3.2 (manual Streamlit QA and documentation): 1.5 h
|
||||
- PR polish, CI tweaks, and addressing review comments: 2.0 h
|
||||
|
||||
Total: 16.5 hours (approx)
|
||||
|
||||
---
|
||||
|
||||
## PR checklist / deliverables
|
||||
- [ ] Unit tests for inspector and centroids pass
|
||||
- [ ] build_trajectories_tab updated with debug expander and fallback
|
||||
- [ ] Integration tests for three scenarios pass (or documented reason for partial coverage)
|
||||
- [ ] Manual Streamlit QA steps documented in PR and verified locally
|
||||
- [ ] Add mention of EXPLORER_MP_FALLBACK_COUNT to README or environment docs (optional follow-up)
|
||||
|
||||
---
|
||||
|
||||
If you'd like, I can now (A) produce the concrete test contents and minimal helper implementations as separate micro-tasks (one file + one test per task) ready for implementers, or (B) proceed to create and apply the code changes in this repo. Which do you prefer?
|
||||
Reference in New Issue
Block a user