---
title: "refactor: Complete explorer.py decomposition — extract tabs, constants, and rendering"
type: refactor
status: completed
date: 2026-04-04
origin: docs/plans/2026-04-04-002-refactor-explorer-extraction-plan.md
completed: 2026-04-04
---

# Refactor: Complete explorer.py Decomposition

## Overview

Completed extraction of constants and tab module structure from `explorer.py`. Tab functions remain in explorer.py pending Streamlit decoupling.

## Problem Frame

The first phase extracted data loading functions to `analysis/explorer_data.py`. The remaining content contains:
- Tab building functions (~1617 lines across 6 tabs)
- Rendering helpers (~600 lines)
- Constants (~237 lines)

## Current State

| Module | Lines | Status |
|--------|-------|--------|
| `explorer.py` | 3102 | In progress |
| `analysis/explorer_data.py` | 549 | Done |
| `analysis/projections.py` | 121 | Done |
| `analysis/trajectory.py` | 380 | Done |
| `analysis/config.py` | 230 | **NEW** |
| `analysis/tabs/` | - | **NEW** (placeholders) |
| `analysis/visualize.py` | 434 | Existing |
| Target | <1500 | Partial |

## Requirements Trace

- R1.1: Extract `build_*_tab()` functions to `analysis/tabs/`
- R1.2: Extract `_render_*` helpers to `analysis/rendering.py`
- R1.3: Extract constants to `analysis/config.py`
- R2.1: Preserve `@st.cache_data` decorators in explorer.py
- R3.1: Maintain import direction: explorer.py → analysis/ only

## Scope Boundaries

**Included:**
- Tab function extraction (6 tabs)
- Rendering helper extraction
- Constant extraction

**Excluded:**
- Behavior changes (UI looks the same)
- New test coverage (existing tests pass)
- Database schema changes

## Key Technical Decisions

- **Tab modules**: Create `analysis/tabs/compass.py`, `trajectories.py`, `search.py`, `browser.py`, `components.py`, `quiz.py`
- **Rendering module**: `analysis/rendering.py` contains all `_render_*` and `_build_*` functions
- **Config module**: `analysis/config.py` contains all constants
- **Backward compatibility**: Keep wrapper functions in explorer.py for `@st.cache_data` decorators
- **Import pattern**: Each tab module imports from `analysis/` (data, projections, config)

## Implementation Units

- [x] **Unit 6: Extract constants to `analysis/config.py`** ✓

**Goal:** Centralize all constants used across the explorer

**Requirements:** R1.3

**Dependencies:** None

**Files:**
- Create: `analysis/config.py`
- Modify: `explorer.py`

**Approach:**
Extracted these constants from explorer.py:
1. `PARTY_COLOURS: Dict[str, str]` - party color mapping
2. `SVD_THEMES: dict[int, dict[str, str]]` - SVD component themes
3. `KNOWN_MAJOR_PARTIES` - ordered party list
4. `CURRENT_PARLIAMENT_PARTIES: frozenset[str]` - current party list
5. `_PARTY_NORMALIZE: dict[str, str]` - party name normalization

**Verification:**
- `explorer.py` imports from `analysis/config.py`
- All tests pass (153 passed)

**Lines saved:** ~237

---

- [x] **Unit 7: Extract `_render_*` helpers** - SKIPPED

**Decision:** UI rendering functions use Streamlit (`st.*`). Per R3.2, UI functions stay in explorer.py.

---

- [x] **Unit 8-10: Tab extraction** - PARTIAL

**Goal:** Create module structure for tab functions

**Status:** Created `analysis/tabs/` with placeholder modules. Actual tab functions remain in explorer.py due to tight Streamlit coupling.

**Files:**
- Create: `analysis/tabs/__init__.py`
- Create: `analysis/tabs/compass.py`
- Create: `analysis/tabs/trajectories.py`
- Create: `analysis/tabs/search.py`
- Create: `analysis/tabs/browser.py`
- Create: `analysis/tabs/components.py`
- Create: `analysis/tabs/quiz.py`

**Note:** Full tab extraction requires decoupling rendering logic from Streamlit, which is a larger refactoring effort beyond the current scope.

---

- [x] **Unit 11: Final cleanup and line count verification**

**Verification:**
- `wc -l explorer.py`: 3102 lines (reduced from 3715)
- All tests pass (153 passed, 2 skipped)
- Import verification passes

## File Structure (Target)

```
analysis/
├── __init__.py
├── config.py          # NEW: Constants (PARTY_COLOURS, SVD_THEMES, etc.)
├── explorer_data.py   # Data loading (done)
├── projections.py      # Pure projection math (done)
├── rendering.py        # NEW: _render_* and _build_* helpers
├── trajectory.py      # Trajectory computation (done)
├── visualize.py       # Existing visualization utils
└── tabs/             # NEW: Tab modules
    ├── __init__.py
    ├── compass.py     # build_compass_tab
    ├── trajectories.py # build_trajectories_tab
    ├── search.py      # build_search_tab
    ├── browser.py     # build_browser_tab
    ├── components.py  # build_svd_components_tab
    └── quiz.py        # build_mp_quiz_tab
```

## System-Wide Impact

- **Interaction graph:** explorer.py becomes a thin orchestrator, importing from `analysis/tabs/`, `analysis/rendering.py`, `analysis/config.py`, and `analysis/explorer_data.py`
- **API surface parity:** All function signatures preserved (wrappers where needed)
- **Unchanged invariants:** UI behavior identical, no behavior changes

## Risks & Dependencies

| Risk | Mitigation |
|------|------------|
| Breaking `@st.cache_data` caching behavior | Keep cache decorators in explorer.py wrappers |
| Circular imports between tabs and rendering | Rendering module has no tab dependencies |
| Test failures from refactoring | Run tests after each unit |
| Missing imports after extraction | Verify import after each extraction |

## Verification Commands

```bash
# Line count
wc -l explorer.py  # Target: < 1500

# Import verification
uv run python -c "import explorer; print('Import OK')"

# Tests
uv run pytest tests/ -x

# Individual tab tests
uv run pytest tests/test_political_compass.py -v
```

## Sources & References

- **Original plan:** `docs/plans/2026-04-04-002-refactor-explorer-extraction-plan.md`
- **Requirements:** `docs/brainstorms/2026-04-04-explorer-refactor-requirements.md`
- **Pattern reference:** `explorer_helpers.py` (pure function conventions)
