diff --git a/explorer.py b/explorer.py index 4e49275..79fa009 100644 --- a/explorer.py +++ b/explorer.py @@ -170,6 +170,46 @@ def get_uniform_dim_windows(db_path: str) -> List[str]: con.close() +def _should_swap_axes(axis_def: dict) -> bool: + """Return True if the Y axis is 'Links–Rechts' and the X axis is not. + + When true, caller should swap x/y positions and metadata so left-right + is conventionally on the horizontal axis. + """ + lr = "Links\u2013Rechts" + return axis_def.get("y_label") == lr and axis_def.get("x_label") != lr + + +def _swap_axes( + positions_by_window: dict, + axis_def: dict, +) -> tuple: + """Swap x and y in all positions and axis metadata. + + Pure function — returns (new_positions_by_window, new_axis_def). + """ + new_positions: dict = {} + for wid, pos_dict in positions_by_window.items(): + new_positions[wid] = {ent: (y, x) for ent, (x, y) in pos_dict.items()} + + new_ax = dict(axis_def) + # Swap paired scalar keys + new_ax["x_label"] = axis_def.get("y_label") + new_ax["y_label"] = axis_def.get("x_label") + + # Swap paired dict keys + for x_key, y_key in [ + ("x_quality", "y_quality"), + ("x_interpretation", "y_interpretation"), + ("x_top_motions", "y_top_motions"), + ("x_label_confidence", "y_label_confidence"), + ]: + new_ax[x_key] = axis_def.get(y_key) + new_ax[y_key] = axis_def.get(x_key) + + return new_positions, new_ax + + @st.cache_data(show_spinner="2D posities berekenen (kan even duren)…") def load_positions( db_path: str, window_size: str = "quarterly" @@ -210,6 +250,9 @@ def load_positions( "classify_axes failed; using generic axis labels" ) + if _should_swap_axes(axis_def): + positions_by_window, axis_def = _swap_axes(positions_by_window, axis_def) + # Filter displayed windows by window_size AFTER PCA computation. if window_size == "annual": annual_keys = set(w for w in all_available if "-Q" not in w) diff --git a/tests/test_political_compass.py b/tests/test_political_compass.py index 798d045..760702e 100644 --- a/tests/test_political_compass.py +++ b/tests/test_political_compass.py @@ -111,6 +111,21 @@ except Exception: _go.Figure = lambda *a, **kw: None sys.modules["plotly.graph_objects"] = _go +# Provide a minimal streamlit stub so explorer imports succeed in the test env +try: + import streamlit as _st # noqa: F401 +except Exception: + _st_stub = types.ModuleType("streamlit") + _st_stub.cache_data = lambda **kw: lambda f: f + _st_stub.plotly_chart = lambda *a, **kw: None + _st_stub.markdown = lambda *a, **kw: None + _st_stub.caption = lambda *a, **kw: None + _st_stub.error = lambda *a, **kw: None + _st_stub.warning = lambda *a, **kw: None + _st_stub.info = lambda *a, **kw: None + _st_stub.write = lambda *a, **kw: None + sys.modules["streamlit"] = _st_stub + import pytest @@ -654,3 +669,58 @@ def test_classify_from_titles_low_confidence(): label, confidence = _classify_from_titles(titles) assert label is None assert confidence < 0.4 + + +def test_axis_swap_when_y_is_left_right(): + """When y_label is 'Links–Rechts' and x_label is not, positions must be swapped.""" + from explorer import _swap_axes + + positions_by_window = { + "2023": { + "VVD": (0.5, 0.8), + "PvdA": (-0.3, -0.6), + } + } + axis_def = { + "x_label": "Progressief\u2013Conservatief", + "y_label": "Links\u2013Rechts", + "x_quality": {"2023": 0.7}, + "y_quality": {"2023": 0.8}, + "x_interpretation": {"2023": "prog interpretation"}, + "y_interpretation": {"2023": "lr interpretation"}, + "x_top_motions": {"2023": {"+": [], "-": []}}, + "y_top_motions": {"2023": {"+": [], "-": []}}, + "x_label_confidence": {"2023": 0.5}, + "y_label_confidence": {"2023": 0.7}, + } + + new_pos, new_ax = _swap_axes(positions_by_window, axis_def) + + # Positions swapped: (x, y) → (y, x) + assert new_pos["2023"]["VVD"] == (0.8, 0.5) + assert new_pos["2023"]["PvdA"] == (-0.6, -0.3) + + # Labels swapped + assert new_ax["x_label"] == "Links\u2013Rechts" + assert new_ax["y_label"] == "Progressief\u2013Conservatief" + + # Quality swapped + assert new_ax["x_quality"] == {"2023": 0.8} + assert new_ax["y_quality"] == {"2023": 0.7} + + +def test_axis_swap_not_applied_when_x_is_left_right(): + """When x_label is already 'Links–Rechts', no swap should occur.""" + from explorer import _should_swap_axes + + axis_def = { + "x_label": "Links\u2013Rechts", + "y_label": "Progressief\u2013Conservatief", + } + assert _should_swap_axes(axis_def) is False + + axis_def2 = { + "x_label": "Links\u2013Rechts", + "y_label": "Links\u2013Rechts", # both LR — no swap + } + assert _should_swap_axes(axis_def2) is False