(function () { 'use strict'; var ID_RE = /^[A-Z]\d{6}$|^S\d{3,4}$/; var METRIC_LABELS = { total: 'Total Votes', yeas: 'Yeas', nays: 'Nays', participation_pct: 'Participation %', voted_with_gop: 'Voted With GOP', voted_with_gop_pct: 'Voted With GOP %', voted_with_dem: 'Voted With Dem', voted_with_dem_pct: 'Voted With Dem %', voted_against_gop: 'Voted Against GOP', voted_against_gop_pct: 'Voted Against GOP %', voted_against_dem: 'Voted Against Dem', voted_against_dem_pct: 'Voted Against Dem %', lone_wolf: 'Lone Wolf Votes', lone_wolf_pct: 'Lone Wolf %' }; var state = { BASE: './data/', manifest: null, chamber: 'H', parties: { R: true, D: true, I: true }, metric: 'total', order: 'desc' }; var els = {}; document.addEventListener('DOMContentLoaded', init); function init() { els.root = document.getElementById('polisci-root'); state.BASE = els.root.dataset.base || './data/'; els.tbody = document.querySelector('#rank-table tbody'); els.thead = document.querySelector('#rank-table thead'); els.metricHeader = document.getElementById('rank-metric-header'); els.summary = document.getElementById('rank-summary'); els.metricSelect = document.getElementById('rank-metric'); els.lastGenerated = document.getElementById('last-generated'); var inline = document.getElementById('polisci-manifest'); if (inline && inline.textContent) { try { onManifest(JSON.parse(inline.textContent)); return; } catch (e) { /* fall through */ } } fetch(state.BASE + 'manifest.json', { credentials: 'omit' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(onManifest) .catch(function (e) { showFatal('Failed to load manifest: ' + e.message + ' — if you opened this file directly, serve it over HTTP.'); }); } function showFatal(msg) { els.root.replaceChildren(); var p = document.createElement('p'); p.style.padding = '20px'; p.style.color = '#c0392b'; p.textContent = msg; els.root.appendChild(p); } function onManifest(m) { state.manifest = m; if (els.lastGenerated) els.lastGenerated.textContent = m.generated_at || ''; var chamberInputs = document.querySelectorAll('input[name="chamber"]'); for (var i = 0; i < chamberInputs.length; i++) { chamberInputs[i].addEventListener('change', onChamberChange); } var partyInputs = document.querySelectorAll('input[name="party"]'); for (var j = 0; j < partyInputs.length; j++) { partyInputs[j].addEventListener('change', onPartyChange); } els.metricSelect.addEventListener('change', function () { state.metric = els.metricSelect.value; syncURL(); render(); }); var orderInputs = document.querySelectorAll('input[name="order"]'); for (var k = 0; k < orderInputs.length; k++) { orderInputs[k].addEventListener('change', onOrderChange); } document.getElementById('rank-reset').addEventListener('click', resetControls); var toggle = document.getElementById('sidebar-toggle'); if (toggle) toggle.addEventListener('click', function () { var body = document.getElementById('sidebar-body'); var open = body.classList.toggle('is-collapsed') ? 'false' : 'true'; toggle.setAttribute('aria-expanded', open); }); window.addEventListener('popstate', function () { restoreFromURL(); render(); }); restoreFromURL(); render(); } function onChamberChange(e) { state.chamber = e.target.value === 'S' ? 'S' : 'H'; syncURL(); render(); } function onPartyChange(e) { state.parties[e.target.value] = e.target.checked; syncURL(); render(); } function onOrderChange(e) { state.order = e.target.value === 'asc' ? 'asc' : 'desc'; syncURL(); render(); } function resetControls() { state.chamber = 'H'; state.parties = { R: true, D: true, I: true }; state.metric = 'total'; state.order = 'desc'; document.querySelector('input[name="chamber"][value="H"]').checked = true; document.querySelector('input[name="party"][value="R"]').checked = true; document.querySelector('input[name="party"][value="D"]').checked = true; document.querySelector('input[name="party"][value="I"]').checked = true; els.metricSelect.value = 'total'; document.querySelector('input[name="order"][value="desc"]').checked = true; syncURL(); render(); } function computeMetric(k, key) { var voting = k.voting || 0; switch (key) { case 'total': return k.total || 0; case 'yeas': return k.yeas || 0; case 'nays': return k.nays || 0; case 'participation_pct': return k.total ? (100 * voting / k.total) : null; case 'voted_with_gop': return k.voted_with_gop || 0; case 'voted_with_gop_pct': return voting ? (100 * (k.voted_with_gop || 0) / voting) : null; case 'voted_with_dem': return k.voted_with_dem || 0; case 'voted_with_dem_pct': return voting ? (100 * (k.voted_with_dem || 0) / voting) : null; case 'voted_against_gop': return k.voted_against_gop || 0; case 'voted_against_gop_pct': return voting ? (100 * (k.voted_against_gop || 0) / voting) : null; case 'voted_against_dem': return k.voted_against_dem || 0; case 'voted_against_dem_pct': return voting ? (100 * (k.voted_against_dem || 0) / voting) : null; case 'lone_wolf': return k.lone_wolf || 0; case 'lone_wolf_pct': return voting ? (100 * (k.lone_wolf || 0) / voting) : null; } return null; } function isPct(key) { return /_pct$/.test(key); } function render() { var members = (state.manifest && state.manifest.members) || []; var rows = []; for (var i = 0; i < members.length; i++) { var m = members[i]; if (m.c !== state.chamber) continue; if (!state.parties[m.p]) continue; if (!m.k) continue; var v = computeMetric(m.k, state.metric); if (v === null) continue; rows.push({ m: m, v: v }); } rows.sort(function (a, b) { if (a.v === b.v) return (a.m.n || '').localeCompare(b.m.n || ''); return state.order === 'asc' ? a.v - b.v : b.v - a.v; }); var label = METRIC_LABELS[state.metric] || state.metric; els.metricHeader.textContent = label; els.summary.replaceChildren(); var p = document.createElement('p'); p.className = 'rank-summary-line'; var chamberName = state.chamber === 'H' ? 'House' : 'Senate'; var partyList = ['R', 'D', 'I'].filter(function (x) { return state.parties[x]; }).join('/') || 'none'; p.textContent = rows.length + ' ' + chamberName + ' members (' + partyList + ') ranked by ' + label + ' (' + (state.order === 'desc' ? 'highest first' : 'lowest first') + ')'; els.summary.appendChild(p); els.tbody.replaceChildren(); var pct = isPct(state.metric); for (var r = 0; r < rows.length; r++) { var row = rows[r]; var tr = document.createElement('tr'); tr.className = 'rank-row'; tr.tabIndex = 0; tr.dataset.id = row.m.id; tr.addEventListener('click', openMember); tr.addEventListener('keydown', function (e) { if (e.key === 'Enter') openMember.call(e.currentTarget, e); }); var rank = document.createElement('td'); rank.textContent = String(r + 1); tr.appendChild(rank); var name = document.createElement('td'); name.textContent = row.m.n || row.m.id; tr.appendChild(name); var party = document.createElement('td'); party.textContent = row.m.p || ''; party.className = 'party-cell party-' + (row.m.p || 'X'); tr.appendChild(party); var st = document.createElement('td'); st.textContent = row.m.s || ''; tr.appendChild(st); var val = document.createElement('td'); val.className = 'rank-value'; val.textContent = pct ? row.v.toFixed(1) + '%' : String(row.v); tr.appendChild(val); els.tbody.appendChild(tr); } } function openMember(e) { var tr = e.currentTarget; var id = tr.dataset.id; if (!id || !ID_RE.test(id)) return; window.open('app.html?id=' + encodeURIComponent(id), '_blank', 'noopener'); } function syncURL() { var params = new URLSearchParams(); if (state.chamber !== 'H') params.set('c', state.chamber); if (state.metric !== 'total') params.set('m', state.metric); if (state.order !== 'desc') params.set('o', state.order); var pset = ['R', 'D', 'I'].filter(function (x) { return state.parties[x]; }); if (pset.length !== 3) params.set('p', pset.join('')); var q = params.toString(); var url = q ? ('?' + q) : location.pathname; history.replaceState(null, '', url); } function restoreFromURL() { var params = new URLSearchParams(location.search); var c = params.get('c'); if (c === 'S' || c === 'H') state.chamber = c; var m = params.get('m'); if (m && METRIC_LABELS[m]) state.metric = m; var o = params.get('o'); if (o === 'asc' || o === 'desc') state.order = o; var p = params.get('p'); if (p) { state.parties = { R: false, D: false, I: false }; for (var i = 0; i < p.length; i++) { var ch = p.charAt(i); if (ch === 'R' || ch === 'D' || ch === 'I') state.parties[ch] = true; } } // Reflect in controls var ci = document.querySelector('input[name="chamber"][value="' + state.chamber + '"]'); if (ci) ci.checked = true; var pInputs = document.querySelectorAll('input[name="party"]'); for (var j = 0; j < pInputs.length; j++) { pInputs[j].checked = !!state.parties[pInputs[j].value]; } els.metricSelect.value = state.metric; var oi = document.querySelector('input[name="order"][value="' + state.order + '"]'); if (oi) oi.checked = true; } })();