| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- (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;
- }
- })();
|