feat(explorer): finalise SVD tab helper robustness and constants
Include plan: docs/superpowers/plans/2026-03-24-svd-tab-redesign.md
This commit is contained in:
+92
-19
@@ -69,6 +69,26 @@ KNOWN_MAJOR_PARTIES = [
|
||||
]
|
||||
|
||||
|
||||
# Current parliament parties (used for party-level SVD lookups)
|
||||
# Keep both common abbreviations and full names that may appear in the DB
|
||||
CURRENT_PARLIAMENT_PARTIES = frozenset(
|
||||
[
|
||||
"VVD",
|
||||
"PVV",
|
||||
"D66",
|
||||
"GroenLinks-PvdA",
|
||||
"GroenLinks",
|
||||
"PvdA",
|
||||
"CDA",
|
||||
"SP",
|
||||
"NSC",
|
||||
"CU",
|
||||
"ChristenUnie",
|
||||
"BBB",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cached loaders
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -204,10 +224,41 @@ def load_party_axis_scores(db_path: str) -> Dict[str, List[float]]:
|
||||
f"AND entity_id IN ({placeholders})",
|
||||
party_list,
|
||||
).fetchall()
|
||||
return {
|
||||
row[0]: json.loads(row[1]) if isinstance(row[1], str) else list(row[1])
|
||||
for row in rows
|
||||
}
|
||||
|
||||
out: Dict[str, List[float]] = {}
|
||||
for row in rows:
|
||||
party = row[0]
|
||||
vec_field = row[1]
|
||||
try:
|
||||
if vec_field is None:
|
||||
# skip missing vectors
|
||||
continue
|
||||
# string-encoded JSON vector
|
||||
if isinstance(vec_field, str):
|
||||
vec = json.loads(vec_field)
|
||||
# bytes (some DB drivers may return bytes)
|
||||
elif isinstance(vec_field, (bytes, bytearray)):
|
||||
try:
|
||||
vec = json.loads(vec_field.decode("utf-8"))
|
||||
except Exception:
|
||||
# fallback: attempt to eval as list-like
|
||||
vec = list(vec_field)
|
||||
# already a list/tuple/np.ndarray-like
|
||||
elif isinstance(vec_field, (list, tuple, np.ndarray)):
|
||||
vec = list(vec_field)
|
||||
else:
|
||||
# unknown type: attempt best-effort conversion
|
||||
vec = list(vec_field)
|
||||
|
||||
# ensure all entries are floats
|
||||
vec_floats = [float(x) for x in vec]
|
||||
out[party] = vec_floats
|
||||
except Exception:
|
||||
# skip malformed rows but keep processing others
|
||||
logger.debug("Skipping malformed vector for party %s", party)
|
||||
continue
|
||||
|
||||
return out
|
||||
except Exception:
|
||||
logger.exception("Failed to load party axis scores")
|
||||
return {}
|
||||
@@ -249,55 +300,77 @@ def _render_party_axis_chart(
|
||||
"""
|
||||
# Validate component selection
|
||||
if not isinstance(comp_sel, int) or comp_sel < 1:
|
||||
st.caption("_Ongeldige SVD-as geselecteerd._")
|
||||
st.caption("Ongeldige SVD-as geselecteerd.")
|
||||
return
|
||||
|
||||
if not party_scores:
|
||||
st.caption("_Partijdata niet beschikbaar_")
|
||||
st.caption("Partijdata zijn niet beschikbaar.")
|
||||
return
|
||||
|
||||
axis_idx = comp_sel - 1
|
||||
|
||||
# Determine maximum available vector dimension to validate selection
|
||||
max_dim = 0
|
||||
for v in party_scores.values():
|
||||
try:
|
||||
if isinstance(v, (list, tuple, np.ndarray)):
|
||||
max_dim = max(max_dim, len(v))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if axis_idx >= max_dim:
|
||||
st.caption(
|
||||
f"Geselecteerde component ({comp_sel}) valt buiten het bereik van de beschikbare vectoren ({max_dim} dimensies)."
|
||||
)
|
||||
return
|
||||
|
||||
parties: List[str] = []
|
||||
xs: List[float] = []
|
||||
|
||||
for party, vec in party_scores.items():
|
||||
# Ensure vec is indexable/sequence-like
|
||||
if not isinstance(vec, (list, tuple, np.ndarray)):
|
||||
# skip malformed entries
|
||||
continue
|
||||
# safe indexing
|
||||
if axis_idx >= len(vec):
|
||||
continue
|
||||
try:
|
||||
raw = vec[axis_idx]
|
||||
# Convert to float safely
|
||||
val = float(raw)
|
||||
# filter non-finite values
|
||||
if not np.isfinite(val):
|
||||
continue
|
||||
except Exception:
|
||||
# skip entries that cannot be indexed or converted
|
||||
continue
|
||||
parties.append(party)
|
||||
xs.append(val)
|
||||
|
||||
if not xs:
|
||||
st.caption("_Partijdata niet beschikbaar_")
|
||||
st.caption("Geen bruikbare partijposities gevonden voor de gekozen SVD-as.")
|
||||
return
|
||||
|
||||
try:
|
||||
x_min = min(xs)
|
||||
x_max = max(xs)
|
||||
x_min = float(min(xs))
|
||||
x_max = float(max(xs))
|
||||
except Exception:
|
||||
st.caption("_Onvoldoende gegevens om asbereik te berekenen_")
|
||||
st.caption("Onvoldoende gegevens om het asbereik te berekenen.")
|
||||
return
|
||||
|
||||
# If min == max, apply symmetric padding around the value.
|
||||
# Symmetric padding around the midpoint for balanced visualisation
|
||||
if x_min == x_max:
|
||||
padding = 0.5 if x_min == 0 else abs(x_min) * 0.1
|
||||
if padding <= 0:
|
||||
padding = 0.5
|
||||
x_min = x_min - padding
|
||||
x_max = x_max + padding
|
||||
center = x_min
|
||||
half = padding
|
||||
else:
|
||||
# Expand range slightly for visual padding
|
||||
x_min = x_min * 1.15
|
||||
x_max = x_max * 1.15
|
||||
center = (x_min + x_max) / 2.0
|
||||
half = max(abs(x_max - center), abs(center - x_min))
|
||||
# add slight visual padding
|
||||
half = half * 1.15
|
||||
|
||||
x_min = center - half
|
||||
x_max = center + half
|
||||
|
||||
# Build horizontal scatter: y is constant (0) but offset for label placement
|
||||
ys = [0 for _ in xs]
|
||||
|
||||
Reference in New Issue
Block a user