/* global React */ /* ============================= MPS DIRECTORY ============================= */ function MpsPage({ nav, filter }) { const D = window.PT_DATA; const [q, setQ] = React.useState(''); const [party, setParty] = React.useState(filter?.party || 'all'); const [district, setDistrict] = React.useState('all'); const [sort, setSort] = React.useState('speeches'); const filtered = React.useMemo(() => { let list = [...D.MPS]; if (q) list = list.filter(m => m.name.toLowerCase().includes(q.toLowerCase())); if (party !== 'all') list = list.filter(m => m.party.abbr === party); if (district !== 'all') list = list.filter(m => m.district === district); if (sort === 'speeches') list.sort((a,b) => b.speechCount - a.speechCount); else if (sort === 'name') list.sort((a,b) => a.name.localeCompare(b.name)); else if (sort === 'party') list.sort((a,b) => a.party.abbr.localeCompare(b.party.abbr)); return list; }, [q, party, district, sort]); return (
Directory

Members of Parliament

All {D.STATS.totalMps} sitting members of the 10th Parliament. Filter by party or district, or search by name.

setQ(e.target.value)} />
[d, d])]} /> onChange(e.target.value)} style={{ fontFamily: 'var(--sans)', fontSize: 12, border: 0, borderBottom: '1px solid var(--ink-3)', background: 'transparent', padding: '3px 4px', color: 'var(--ink)', outline: 'none' }}> {options.map(([v, l]) => )} ); } /* ============================= TOPICS INDEX ============================= */ function TopicsPage({ nav }) { const D = window.PT_DATA; const sorted = [...D.TOPICS].sort((a,b) => b.count - a.count); const max = Math.max(...D.TOPICS.map(t => t.count)); return (
Index

Policy areas

Eighteen flat categories that cover what gets debated in the chamber. Every speech is tagged by AI with one to three topics.

{sorted.map((t, i) => ( {e.preventDefault();nav('topic',{slug:t.slug});}} style={{ display: 'grid', gridTemplateColumns: '40px 1fr 260px 60px', gap: 18, padding: '16px 0', borderTop: '1px solid var(--rule)', alignItems: 'center' }}> {String(i+1).padStart(2,'0')}
{t.name}
{t.count}
))}
); } /* ============================= SITTINGS INDEX ============================= */ function SittingsPage({ nav }) { const D = window.PT_DATA; return (
Archive

Sittings

A sitting is one day of parliament. The record below includes everything said on the floor of the House.

{[...D.SITTINGS].reverse().map((s, i) => ( nav('sitting', { date: s.date })} style={{ cursor: 'pointer' }}> ))}
# Date Session Speeches Activity
{String(D.SITTINGS.length - i).padStart(2,'0')} {fmtDateLong(s.date)} {s.session} {s.speechCount}
); } /* ============================= SEARCH ============================= */ function SearchPage({ initialQ = '', nav }) { const D = window.PT_DATA; const [q, setQ] = React.useState(initialQ); const [topicFilter, setTopicFilter] = React.useState('all'); const [partyFilter, setPartyFilter] = React.useState('all'); const results = React.useMemo(() => { if (!q) return []; const ql = q.toLowerCase(); return D.SPEECHES.filter(s => { const txt = `${s.summary} ${s.mp.name}`.toLowerCase(); if (!txt.includes(ql)) return false; if (topicFilter !== 'all' && !s.topics.some(t => t.slug === topicFilter)) return false; if (partyFilter !== 'all' && s.mp.party.abbr !== partyFilter) return false; return true; }); }, [q, topicFilter, partyFilter]); const suggestions = ['fertiliser', 'IMF', 'Wilpattu', 'fuel', 'hospital', 'corruption', 'Mullaitivu']; return (
Search the record
setQ(e.target.value)} placeholder="Search 2,018 speeches…" style={{ fontSize: 22, fontFamily: 'var(--serif)' }} autoFocus />
{!q && (
Try {suggestions.map(s => ( ))}
)}
{q && ( <>
{results.length} results for "{q}" [p.abbr, p.abbr])]} />
{results.slice(0, 20).map(s => ( ))} {results.length === 0 && (
No speeches match "{q}" with the current filters.
)}
)}
); } /* ============================= HEATMAP (novel) ============================= */ function HeatmapPage({ nav }) { const D = window.PT_DATA; const [mode, setMode] = React.useState('share'); // share or absolute const visibleParties = D.PARTIES.filter(p => p.seats >= 1).sort((a,b) => b.seats - a.seats); // values: share (%) or absolute count per cell function cellValue(topic, abbr) { const row = D.TOPIC_PARTY_MATRIX.find(r => r.topic === topic.slug); const share = row.values[abbr] || 0; if (mode === 'share') return share; return Math.round(share * topic.count); } function cellColor(topic, abbr) { const row = D.TOPIC_PARTY_MATRIX.find(r => r.topic === topic.slug); const share = row.values[abbr] || 0; // intensity relative to max share in this row const maxInRow = Math.max(...visibleParties.map(p => row.values[p.abbr] || 0)); const t = maxInRow > 0 ? share / maxInRow : 0; if (t < 0.05) return 'var(--hm-0)'; if (t < 0.15) return 'var(--hm-1)'; if (t < 0.35) return 'var(--hm-2)'; if (t < 0.6) return 'var(--hm-3)'; if (t < 0.85) return 'var(--hm-4)'; return 'var(--hm-5)'; } const [hover, setHover] = React.useState(null); return (
Novel view

Who talks about what?

A party-by-topic heatmap. Each cell shows the share of speeches tagged with that topic that came from MPs of a given party — so you can see who owns each issue.

Intensity is relative per topic row · darker = disproportionate focus {hover ? `${hover.topic.name} × ${hover.party.name} · ${(hover.share * 100).toFixed(0)}% share, ${Math.round(hover.share * hover.topic.count)} speeches` : 'hover any cell'}
{visibleParties.map(p => ( ))} {D.TOPICS.map(topic => { const row = D.TOPIC_PARTY_MATRIX.find(r => r.topic === topic.slug); return ( {visibleParties.map(p => { const share = row.values[p.abbr] || 0; const v = cellValue(topic, p.abbr); return ( ); })} ); })}
Topic
{p.abbr}
{e.preventDefault();nav('topic',{slug:topic.slug});}} className="serif" style={{ fontSize: 14, fontWeight: 600 }}> {topic.short}
{topic.count}
setHover({ topic, party: p, share })} onMouseLeave={() => setHover(null)}>
{mode === 'share' ? (share > 0.05 ? (share * 100).toFixed(0) : '') : (v > 2 ? v : '')}
Scale {['hm-0','hm-1','hm-2','hm-3','hm-4','hm-5'].map((c, i) => ( {i === 0 ? '0' : i === 5 ? 'max' : ''} ))}
); } Object.assign(window, { MpsPage, TopicsPage, SittingsPage, SearchPage, HeatmapPage });