|
|
# Code Patterns
|
|
|
|
|
|
## 1. Page Wrapper Pattern
|
|
|
Thin Streamlit page files delegate to core modules. Pages contain only route logic, not business logic.
|
|
|
|
|
|
**Example** (pages/1_🗳️_Stemwijzer.py):
|
|
|
```python
|
|
|
import streamlit as st
|
|
|
from quiz_module import render_quiz_page
|
|
|
|
|
|
st.set_page_config(...)
|
|
|
render_quiz_page()
|
|
|
```
|
|
|
|
|
|
**Example** (pages/2_🔍_Explorer.py):
|
|
|
```python
|
|
|
import streamlit as st
|
|
|
from explorer import render_explorer
|
|
|
|
|
|
st.set_page_config(...)
|
|
|
render_explorer()
|
|
|
```
|
|
|
|
|
|
**Rule**: Pages should have <20 lines of logic. All complexity lives in modules.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. Pipeline Pattern
|
|
|
Data flows: fetch → transform → store
|
|
|
|
|
|
**Location**: `pipeline/` directory
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
def run_pipeline():
|
|
|
raw_data = fetch_from_source()
|
|
|
transformed = transform(raw_data)
|
|
|
store(transformed)
|
|
|
|
|
|
def fetch_from_source():
|
|
|
# API call or DB query
|
|
|
...
|
|
|
|
|
|
def transform(raw):
|
|
|
# Clean, normalize, compute derived fields
|
|
|
...
|
|
|
```
|
|
|
|
|
|
**Usage**: SVD computation pipeline, data ingestion, motion processing
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. API Client Pattern
|
|
|
HTTP client with retry/backoff for external data sources.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
import time
|
|
|
import requests
|
|
|
|
|
|
def fetch_with_retry(url, max_retries=3):
|
|
|
for attempt in range(max_retries):
|
|
|
try:
|
|
|
response = requests.get(url)
|
|
|
response.raise_for_status()
|
|
|
return response.json()
|
|
|
except requests.RequestException:
|
|
|
if attempt < max_retries - 1:
|
|
|
time.sleep(2 ** attempt) # exponential backoff
|
|
|
else:
|
|
|
raise
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 4. Pure Helper Functions
|
|
|
Functions in `explorer_helpers.py` have no side effects, no IO.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
def compute_party_coords(svd_df, party_map, window):
|
|
|
"""Pure function: same inputs → same outputs, no side effects."""
|
|
|
# Filter, compute, return
|
|
|
return result_df
|
|
|
|
|
|
def build_scatter_trace(df, color_col, marker_size=8):
|
|
|
"""Pure: returns Plotly trace dict, no rendering."""
|
|
|
trace = go.Scatter(x=df.x, y=df.y, mode='markers', ...)
|
|
|
return trace
|
|
|
```
|
|
|
|
|
|
**Rule**: No `import streamlit` in helper modules. No file I/O. No global state.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 5. Dummy Fallbacks for Optional Dependencies
|
|
|
Gracefully degrade when optional packages are unavailable.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
try:
|
|
|
import umap
|
|
|
HAS_UMAP = True
|
|
|
except ImportError:
|
|
|
HAS_UMAP = False
|
|
|
# or provide dummy stub
|
|
|
|
|
|
def project_to_2d(vectors):
|
|
|
if HAS_UMAP:
|
|
|
return umap.UMAP().fit_transform(vectors)
|
|
|
else:
|
|
|
return vectors[:, :2] # fallback: just take first 2 dims
|
|
|
```
|
|
|
|
|
|
**Used for**: UMAP, Plotly (with fallback to altair or text-only)
|
|
|
|
|
|
---
|
|
|
|
|
|
## 6. Cached Data Loaders
|
|
|
Expensive DB queries wrapped with `@st.cache_data`.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
@st.cache_data
|
|
|
def load_svd_vectors(window: str) -> pd.DataFrame:
|
|
|
return db.query("SELECT * FROM svd_vectors WHERE window = ?", window)
|
|
|
|
|
|
@st.cache_data
|
|
|
def load_party_centroids(window: str) -> pd.DataFrame:
|
|
|
return db.query("SELECT * FROM party_centroids WHERE window = ?", window)
|
|
|
|
|
|
# Clear cache when data updates
|
|
|
@st.cache_data
|
|
|
def load_motions(category: str | None = None) -> pd.DataFrame:
|
|
|
...
|
|
|
```
|
|
|
|
|
|
**Rule**: Use `ttl=3600` for large datasets. Use `show_spinner=False` where appropriate.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 7. Plotly Dual-Layer Charts
|
|
|
Charts built with two traces: scatter points + text annotations.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
def build_dual_layer_chart(df, x_col, y_col, label_col):
|
|
|
# Layer 1: markers
|
|
|
scatter = go.Scatter(
|
|
|
x=df[x_col], y=df[y_col],
|
|
|
mode='markers',
|
|
|
marker=dict(size=10, color=df['color']),
|
|
|
name='Parties'
|
|
|
)
|
|
|
# Layer 2: labels (smaller, non-hoverable)
|
|
|
labels = go.Scatter(
|
|
|
x=df[x_col], y=df[y_col],
|
|
|
mode='text',
|
|
|
text=df[label_col],
|
|
|
textposition='top center',
|
|
|
showlegend=False
|
|
|
)
|
|
|
return [scatter, labels]
|
|
|
```
|
|
|
|
|
|
**Used in**: Explorer tab charts, party position plots
|
|
|
|
|
|
---
|
|
|
|
|
|
## 8. Singleton Module Instances
|
|
|
One shared instance per module, created at import time.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
# database.py
|
|
|
class MotionDatabase:
|
|
|
def __init__(self, db_path=None):
|
|
|
self.conn = ibis.duckdb.connect(db_path)
|
|
|
self._load_schema()
|
|
|
|
|
|
_db = None
|
|
|
def get_db():
|
|
|
global _db
|
|
|
if _db is None:
|
|
|
_db = MotionDatabase()
|
|
|
return _db
|
|
|
|
|
|
# At module bottom:
|
|
|
db = MotionDatabase() # singleton instance
|
|
|
```
|
|
|
|
|
|
**Also used in**: `config.py` exports `config` and `PARTY_COLOURS`
|
|
|
|
|
|
---
|
|
|
|
|
|
## 9. Dataclass Config Pattern
|
|
|
Configuration centralized in a `@dataclass`.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
|
class Config:
|
|
|
db_path: str = "data/stemwijzer.duckdb"
|
|
|
default_window: str = "2023"
|
|
|
cache_ttl: int = 3600
|
|
|
party_colours: dict = field(default_factory=lambda: PARTY_COLOURS)
|
|
|
|
|
|
def __post_init__(self):
|
|
|
if not Path(self.db_path).exists():
|
|
|
raise FileNotFoundError(f"Database not found: {self.db_path}")
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 10. Graceful Degradation with try/except
|
|
|
Core pattern throughout: attempt operation, fall back gracefully.
|
|
|
|
|
|
**Pattern**:
|
|
|
```python
|
|
|
def get_political_position(mp_name, window):
|
|
|
try:
|
|
|
vectors = load_svd_vectors(window)
|
|
|
return vectors[vectors['mp_name'] == mp_name]['vector_2d'].iloc[0]
|
|
|
except (KeyError, IndexError):
|
|
|
return [0.0, 0.0] # neutral fallback
|
|
|
```
|
|
|
|