@ -251,21 +251,27 @@ def load_party_axis_scores(db_path: str) -> Dict[str, List[float]]:
def _render_party_axis_chart (
party_scores : Dict [ str , List [ float ] ] , comp_sel : int
party_scores : Dict [ str , List [ float ] ] , comp_sel : int , theme : dict
) - > None :
""" Render a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`.
Each party is plotted at its score on a single horizontal axis ( y = 0 ) .
When theme [ ' flip ' ] is True the scores are negated so that the progressive / left
side always appears on the left of the chart .
"""
if not party_scores :
st . caption ( " _Partijdata niet beschikbaar voor deze as._ " )
return
axis_idx = comp_sel - 1 # 0-based index into the 50-dim vector
flip = theme . get ( " flip " , False )
data : list [ dict ] = [ ]
for party , vec in party_scores . items ( ) :
if axis_idx < len ( vec ) :
data . append ( { " party " : party , " score " : vec [ axis_idx ] } )
score = vec [ axis_idx ]
if flip :
score = - score
data . append ( { " party " : party , " score " : score } )
if not data :
st . caption ( " _Geen partijscores voor deze as._ " )
@ -276,9 +282,17 @@ def _render_party_axis_chart(
colours = [ PARTY_COLOURS . get ( p , " #9E9E9E " ) for p in parties ]
hover = [ f " { p } : { s : .3f } " for p , s in zip ( parties , scores ) ]
# Determine axis labels: left = progressive pole, right = conservative pole
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
fig = go . Figure ( )
# Baseline
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
fig . add_trace (
go . Scatter (
x = [ x_min , x_max ] ,
@ -297,7 +311,7 @@ def _render_party_axis_chart(
mode = " markers+text " ,
text = parties ,
textposition = " top center " ,
marker = { " size " : 12 , " color " : colours } ,
marker = { " size " : 18 , " color " : colours } ,
hovertext = hover ,
hoverinfo = " text " ,
showlegend = False ,
@ -307,12 +321,13 @@ def _render_party_axis_chart(
height = 160 ,
margin = { " l " : 10 , " r " : 10 , " t " : 10 , " b " : 30 } ,
xaxis = {
" title " : " ← Negatieve pool | Positieve pool → " ,
" title " : f " ← { left_label } | { right_label } →" ,
" zeroline " : True ,
" zerolinecolor " : " #aaaaaa " ,
} ,
yaxis = { " visible " : False , " range " : [ - 1 , 2 ] } ,
plot_bgcolor = " white " ,
plot_bgcolor = " rgba(0,0,0,0) " ,
paper_bgcolor = " rgba(0,0,0,0) " ,
)
st . plotly_chart ( fig , use_container_width = True )
@ -784,6 +799,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Breed coalitiebeleid: zorg, defensie, multilateralisme, inclusie " ,
" negative_pole " : " Radicale PVV-eis tot onmiddellijke uitzetting migranten " ,
" flip " : True ,
} ,
2 : {
" label " : " Nationalistisch migratiebeleid versus progressief internationaal solidariteitsdenken " ,
@ -799,6 +815,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Asielbeperking, nationaal belang, restrictief migratiebeleid " ,
" negative_pole " : " Pro-Palestina, progressieve zorgrechten, anti-discriminatie minderheden " ,
" flip " : False ,
} ,
3 : {
" label " : " Humanitaire solidariteit en inclusie versus nationalistische handhaving en deregulering " ,
@ -815,6 +832,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Internationale solidariteit, inclusie en pragmatische overheidsinterventie " ,
" negative_pole " : " Strikte handhaving, deregulering en nationalistisch eigenbelang boven humanitaire verplichtingen " ,
" flip " : True ,
} ,
4 : {
" label " : " Publieke voorzieningen beschermen versus liberale marktwerking " ,
@ -830,8 +848,9 @@ def build_svd_components_tab(db_path: str) -> None:
" voorzieningen betaalbaar en toegankelijk te houden, of dat vrije markt en open handel "
" leidend moeten zijn. "
) ,
" positive_pole " : " Staatsbescherming van betaalbare publieke voorzieningen voor iedereen " ,
" negative_pole " : " Vrije handel, open economie en marktgerichte arbeidsmigratie " ,
" positive_pole " : " Vrije handel, open economie en marktgerichte arbeidsmigratie " ,
" negative_pole " : " Staatsbescherming van betaalbare publieke voorzieningen voor iedereen " ,
" flip " : False ,
} ,
5 : {
" label " : " Christelijk-conservatief sociaal beleid versus seculier progressief " ,
@ -844,8 +863,9 @@ def build_svd_components_tab(db_path: str) -> None:
" consistent vanuit een christelijk-sociale visie stemmen tegenover partijen als D66, "
" GroenLinks-PvdA en SP die een seculier-progressief beleid voorstaan. "
) ,
" positive_pole " : " Christelijk-conservatief: gezin, kerk, leven, traditionele waarden " ,
" negative_pole " : " Seculier-progressief: individuele autonomie, progressieve sociale rechten " ,
" positive_pole " : " Seculier-progressief: individuele autonomie, progressieve sociale rechten " ,
" negative_pole " : " Christelijk-conservatief: gezin, kerk, leven, traditionele waarden " ,
" flip " : True ,
} ,
6 : {
" label " : " Christelijk-sociaal beschermingsbeleid versus links-progressieve systeemkritiek " ,
@ -860,6 +880,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Christelijk-sociaal beschermingsbeleid voor pgb, kinderen en geloofsgroepen " ,
" negative_pole " : " Links-progressieve systeemkritiek op zorg, arbeid en internationale solidariteit " ,
" flip " : False ,
} ,
7 : {
" label " : " Liberaal investeren en defensie versus linkse bescherming en controle " ,
@ -873,6 +894,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Gerichte liberale investeringen in sport, wetenschap en defensie " ,
" negative_pole " : " Collectieve bescherming, parlementaire controle en anti-marktwerking in zorg " ,
" flip " : False ,
} ,
8 : {
" label " : " Confessioneel-sociaal coalitiebeleid versus procedurele blokkade en handhaving " ,
@ -888,6 +910,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Coalitie christelijk-sociaal beleid: defensie, stikstofmaatwerk, bouw en ethiek " ,
" negative_pole " : " Procedurele blokkade coffeeshop, handhavingsdoelstelling en topsportderegulering " ,
" flip " : False ,
} ,
9 : {
" label " : " Brede coalitiemeerderheid versus links marktingrijpen zorg " ,
@ -903,6 +926,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Breed gedragen beleid door centrum-rechts meerderheidsstemmen " ,
" negative_pole " : " Socialistische inkomensregulering en marktingrijpen in de zorg " ,
" flip " : False ,
} ,
10 : {
" label " : " Gereguleerde kennismigratie en natuur-landbouwtransitie versus institutionele veiligheid " ,
@ -918,6 +942,7 @@ def build_svd_components_tab(db_path: str) -> None:
) ,
" positive_pole " : " Beperkte kennismigratie, natuur-landbouwtransitie en Gaza-humanitair " ,
" negative_pole " : " Institutionele veiligheidssturing, economisch nationalisme en Woo-beperking " ,
" flip " : True ,
} ,
}
@ -985,7 +1010,7 @@ def build_svd_components_tab(db_path: str) -> None:
# Party axis chart
party_scores = load_party_axis_scores ( db_path )
_render_party_axis_chart ( party_scores , comp_sel )
_render_party_axis_chart ( party_scores , comp_sel , theme )
# Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results)
motion_ids = [ m . get ( " motion_id " ) for m in motions if m . get ( " motion_id " ) is not None ]
@ -1021,21 +1046,28 @@ def build_svd_components_tab(db_path: str) -> None:
pos_motions = [ m for m in motions if float ( m . get ( " score " , 0.0 ) ) > = 0 ]
neg_motions = [ m for m in motions if float ( m . get ( " score " , 0.0 ) ) < 0 ]
pos_pole = (
theme . get ( " positive_pole " , " Positieve pool " ) if theme else " Positieve pool "
)
neg_pole = (
theme . get ( " negative_pole " , " Negatieve pool " ) if theme else " Negatieve pool "
)
flip = theme . get ( " flip " , False ) if theme else False
pos_pole = theme . get ( " positive_pole " , " " ) if theme else " "
neg_pole = theme . get ( " negative_pole " , " " ) if theme else " "
# Determine which pole goes left (progressive) and which goes right
if flip :
left_pole , right_pole = pos_pole , neg_pole
left_motions , right_motions = pos_motions , neg_motions
left_arrow , right_arrow = " ▲ " , " ▼ "
else :
left_pole , right_pole = neg_pole , pos_pole
left_motions , right_motions = neg_motions , pos_motions
left_arrow , right_arrow = " ▼ " , " ▲ "
pcol , ncol = st . columns ( 2 )
lcol , r col = st . columns ( 2 )
with pcol :
st . success ( f " ▲ **Positieve pool:** { pos_pole } " )
for m in pos_motions :
with l col:
st . markdown ( f " **← { left_pole } ** " )
for m in left _motions:
mid = m . get ( " motion_id " )
raw_title = m . get ( " title " ) or f " Motie # { mid } "
with st . expander ( f " ▲ { raw_title [ : 80 ] } " ) :
with st . expander ( f " { left_arrow } { raw_title [ : 80 ] } " ) :
row = motion_details . get ( int ( mid ) ) if mid is not None else None
if row :
try :
@ -1052,12 +1084,12 @@ def build_svd_components_tab(db_path: str) -> None:
else :
st . caption ( " _Geen metadata beschikbaar_ " )
with n col:
st . error ( f " ▼ **Negatieve pool:** { neg_pole } " )
for m in neg _motions:
with r col:
st . markdown ( f " ** { right_pole } →** " )
for m in right _motions:
mid = m . get ( " motion_id " )
raw_title = m . get ( " title " ) or f " Motie # { mid } "
with st . expander ( f " ▼ { raw_title [ : 80 ] } " ) :
with st . expander ( f " { right_arrow } { raw_title [ : 80 ] } " ) :
row = motion_details . get ( int ( mid ) ) if mid is not None else None
if row :
try :