refactor: use scatter plot format for SVD components 3-10
- Changed _render_party_axis_chart_1d from horizontal bar chart to scatter plot - Same format as components 1-2: markers on horizontal line with axis arrows- Axis labels now show correct direction with arrows (← left | right →) - Ensures consistent visualization across all SVD components
This commit is contained in:
+71
-36
@@ -1357,7 +1357,10 @@ def _render_party_axis_chart_1d(
|
||||
comp_sel: int,
|
||||
theme: dict,
|
||||
) -> None:
|
||||
"""Render a 1D horizontal bar chart of party positions on SVD component `comp_sel`.
|
||||
"""Render a 1D horizontal scatter of party positions on SVD component `comp_sel`.
|
||||
|
||||
Uses the same format as components 1-2: parties as markers on a horizontal line
|
||||
with axis title showing poles with arrows.
|
||||
|
||||
Args:
|
||||
party_coords: Dict mapping party name to tuple of scores (score_for_comp,)
|
||||
@@ -1371,53 +1374,85 @@ def _render_party_axis_chart_1d(
|
||||
return
|
||||
|
||||
# Extract scores and parties
|
||||
parties = list(party_coords.keys())
|
||||
scores = [coords[0] for coords in party_coords.values()]
|
||||
parties = []
|
||||
scores = []
|
||||
colours = []
|
||||
|
||||
# Apply flip if needed
|
||||
for party, coords in party_coords.items():
|
||||
try:
|
||||
score = float(coords[0])
|
||||
parties.append(party)
|
||||
scores.append(score)
|
||||
colours.append(PARTY_COLOURS.get(party, "#9E9E9E"))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not scores:
|
||||
st.caption("_Partijdata niet beschikbaar voor deze as._")
|
||||
return
|
||||
|
||||
# Apply flip if needed (ensures right parties appear on right side)
|
||||
flip = theme.get("flip", False)
|
||||
if flip:
|
||||
scores = [-s for s in scores]
|
||||
|
||||
# Get party colors
|
||||
party_colors = [PARTY_COLOURS.get(p, "#9E9E9E") for p in parties]
|
||||
# Build hover text
|
||||
hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)]
|
||||
|
||||
# Sort by score for better visualization
|
||||
sorted_indices = np.argsort(scores)
|
||||
sorted_parties = [parties[i] for i in sorted_indices]
|
||||
sorted_scores = [scores[i] for i in sorted_indices]
|
||||
sorted_colors = [party_colors[i] for i in sorted_indices]
|
||||
|
||||
# Create horizontal bar chart
|
||||
# Create figure with same format as components 1-2
|
||||
fig = go.Figure()
|
||||
x_min, x_max = min(scores) * 1.15, max(scores) * 1.15
|
||||
if x_min == x_max:
|
||||
x_min, x_max = x_min - 1, x_max + 1
|
||||
|
||||
# Add horizontal axis line
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
y=sorted_parties,
|
||||
x=sorted_scores,
|
||||
orientation="h",
|
||||
marker_color=sorted_colors,
|
||||
text=[f"{s:.2f}" for s in sorted_scores],
|
||||
textposition="outside",
|
||||
)
|
||||
)
|
||||
|
||||
# Update layout
|
||||
label = theme.get("label", f"As {comp_sel}")
|
||||
positive_pole = theme.get("positive_pole", "Positief")
|
||||
negative_pole = theme.get("negative_pole", "Negatief")
|
||||
|
||||
fig.update_layout(
|
||||
title=f"Partijposities — {label}",
|
||||
xaxis_title=f"{negative_pole} ← → {positive_pole}",
|
||||
yaxis_title="",
|
||||
height=max(400, len(parties) * 25),
|
||||
margin=dict(l=150),
|
||||
go.Scatter(
|
||||
x=[x_min, x_max],
|
||||
y=[0, 0],
|
||||
mode="lines",
|
||||
line={"color": "#cccccc", "width": 1},
|
||||
hoverinfo="skip",
|
||||
showlegend=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Add vertical line at x=0
|
||||
fig.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.5)
|
||||
# Add party markers
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=scores,
|
||||
y=[0] * len(scores),
|
||||
mode="markers+text",
|
||||
text=parties,
|
||||
textposition="top center",
|
||||
marker={"size": 14, "color": colours},
|
||||
hovertext=hover,
|
||||
hoverinfo="text",
|
||||
showlegend=False,
|
||||
)
|
||||
)
|
||||
|
||||
# Determine pole labels based on flip
|
||||
pos_pole = theme.get("positive_pole", "")
|
||||
neg_pole = theme.get("negative_pole", "")
|
||||
left_label = pos_pole if flip else neg_pole
|
||||
right_label = neg_pole if flip else pos_pole
|
||||
|
||||
# Update layout with same format as components 1-2
|
||||
fig.update_layout(
|
||||
height=160,
|
||||
margin={"l": 10, "r": 10, "t": 10, "b": 30},
|
||||
xaxis={
|
||||
"title": f"← {left_label} | {right_label} →",
|
||||
"showticklabels": False,
|
||||
"showline": False,
|
||||
"showgrid": False,
|
||||
"zeroline": False,
|
||||
},
|
||||
yaxis={"visible": False, "range": [-1, 2]},
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ def test_partial_party_traces():
|
||||
|
||||
|
||||
def test_render_party_axis_chart_1d_renders():
|
||||
"""Test that _render_party_axis_chart_1d creates a figure with proper structure."""
|
||||
"""Test that _render_party_axis_chart_1d creates a scatter plot with markers (same format as components 1-2)."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from explorer import _render_party_axis_chart_1d
|
||||
@@ -310,8 +310,12 @@ def test_render_party_axis_chart_1d_renders():
|
||||
# Get the figure passed to plotly_chart
|
||||
fig = mock_plotly_chart.call_args[0][0]
|
||||
assert fig is not None, "Figure should not be None"
|
||||
# Check that figure has traces (the bar chart)
|
||||
assert len(fig.data) > 0, "Figure should have traces"
|
||||
# Check that figure has 2 traces (baseline line + markers)
|
||||
assert len(fig.data) == 2, "Figure should have 2 traces (baseline + markers)"
|
||||
# First trace is the baseline line
|
||||
assert fig.data[0].mode == "lines", "First trace should be a line"
|
||||
# Second trace is the marker scatter
|
||||
assert "markers" in fig.data[1].mode, "Second trace should have markers"
|
||||
|
||||
|
||||
def test_render_party_axis_chart_1d_empty_coords():
|
||||
|
||||
Reference in New Issue
Block a user