(function () { 'use strict'; // Security: validate any member id before fetch / DOM use. var ID_RE = /^[A-Z]\d{6}$|^S\d{3,4}$/; var LS_KEY = 'polisci:v119:lastMember'; var COLORS = { R: '#d9534f', D: '#337ab7', I: '#5cb85c', helpedR: '#d9534f', helpedD: '#337ab7', helpedBoth: '#8e44ad', helpedNeither: '#7f8c8d', green: '#27ae60', red: '#c0392b' }; var ALIGN_KEYS = ['Helped Republicans', 'Helped Democrats', 'Helped Both', 'Helped Neither']; var ALIGN_COLORS = [COLORS.helpedR, COLORS.helpedD, COLORS.helpedBoth, COLORS.helpedNeither]; var state = { manifest: null, membersById: {}, // id -> manifest entry BASE: './data/', currentId: null, currentMember: null, sortCol: null, sortAsc: true, selectedStates: null, // null => all skipPush: false, urlReplaceTimer: 0 }; var els = {}; var charts = {}; document.addEventListener('DOMContentLoaded', init); function init() { var root = document.getElementById('polisci-root'); if (!root) return; state.BASE = root.dataset.base || './data/'; els.root = root; els.note = document.getElementById('member-note'); els.summary = document.getElementById('member-summary'); els.kpis = document.getElementById('kpi-cards'); els.search = document.getElementById('member-search'); els.results = document.getElementById('member-results'); els.lastGen = document.getElementById('last-generated'); els.filterChamber = document.getElementById('filter-chamber'); els.filterParty = document.getElementById('filter-party'); els.filterState = document.getElementById('filter-state-select'); els.filterReset = document.getElementById('filter-reset'); els.sidebarToggle = document.getElementById('sidebar-toggle'); els.sidebar = document.getElementById('sidebar'); els.tableBody = document.querySelector('#vote-table tbody'); els.tableHead = document.querySelector('#vote-table thead'); var inline = document.getElementById('polisci-manifest'); if (inline && inline.textContent) { try { onManifest(JSON.parse(inline.textContent)); return; } catch (e) { /* fall through to fetch */ } } var url = state.BASE + 'manifest.json'; fetch(url, { 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 (e.g., `cd results/119 && python3 -m http.server`).'); }); } 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; var members = m.members || []; for (var i = 0; i < members.length; i++) { state.membersById[members[i].id] = members[i]; } els.lastGen.textContent = m.generated_at || ''; populateStateOptions(members); wireEvents(); buildCharts(); initSortHeaders(); restoreFiltersFromURL(); initialSelect(); window.addEventListener('popstate', onPopState); } function populateStateOptions(members) { var states = {}; for (var i = 0; i < members.length; i++) states[members[i].s] = true; var keys = Object.keys(states).sort(); els.filterState.replaceChildren(); for (var j = 0; j < keys.length; j++) { var opt = document.createElement('option'); opt.value = keys[j]; opt.textContent = keys[j]; opt.selected = true; els.filterState.appendChild(opt); } } function wireEvents() { var chamberInputs = els.filterChamber.querySelectorAll('input[type=checkbox]'); var partyInputs = els.filterParty.querySelectorAll('input[type=checkbox]'); chamberInputs.forEach(function (i) { i.addEventListener('change', onFilterChange); }); partyInputs.forEach(function (i) { i.addEventListener('change', onFilterChange); }); els.filterState.addEventListener('change', onFilterChange); els.filterReset.addEventListener('click', resetFilters); els.search.addEventListener('input', onFilterChange); els.search.addEventListener('focus', refreshFilter); document.addEventListener('click', function (e) { if (!els.results.contains(e.target) && e.target !== els.search) { els.results.classList.add('is-hidden'); els.search.setAttribute('aria-expanded', 'false'); } }); if (els.sidebarToggle) { els.sidebarToggle.addEventListener('click', function () { var open = els.sidebar.classList.toggle('is-open'); els.sidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); }); } } function onFilterChange() { refreshFilter(); debounceUrlReplace(); } function resetFilters() { var inputs = els.filterChamber.querySelectorAll('input[type=checkbox]'); inputs.forEach(function (i) { i.checked = true; }); inputs = els.filterParty.querySelectorAll('input[type=checkbox]'); inputs.forEach(function (i) { i.checked = true; }); for (var j = 0; j < els.filterState.options.length; j++) { els.filterState.options[j].selected = true; } els.search.value = ''; refreshFilter(); debounceUrlReplace(); } function getCheckedValues(group) { var out = []; var inputs = group.querySelectorAll('input[type=checkbox]:checked'); inputs.forEach(function (i) { out.push(i.value); }); return out; } function getSelectedStates() { var out = []; for (var i = 0; i < els.filterState.options.length; i++) { if (els.filterState.options[i].selected) out.push(els.filterState.options[i].value); } return out; } function initialsOf(name) { var parts = name.split(/\s+/); var out = ''; for (var i = 0; i < parts.length; i++) { if (parts[i]) out += parts[i].charAt(0).toUpperCase(); } return out; } function refreshFilter() { var chambers = getCheckedValues(els.filterChamber); var parties = getCheckedValues(els.filterParty); var states = getSelectedStates(); var q = (els.search.value || '').trim(); var qLower = q.toLowerCase(); var qUpper = q.toUpperCase(); var initialsMode = q.length > 0 && q.length <= 4 && /^[A-Za-z]+$/.test(q); var members = state.manifest.members; var matches = []; for (var i = 0; i < members.length && matches.length < 50; i++) { var m = members[i]; if (chambers.indexOf(m.c) < 0) continue; if (parties.indexOf(m.p) < 0) continue; if (states.indexOf(m.s) < 0) continue; if (q) { var nameLower = m.n.toLowerCase(); var hit = nameLower.indexOf(qLower) >= 0; if (!hit && initialsMode) { if (initialsOf(m.n) === qUpper) hit = true; } if (!hit) continue; } matches.push(m); } matches.sort(function (a, b) { return a.n.localeCompare(b.n); }); renderResults(matches, q); } function renderResults(matches, q) { els.results.replaceChildren(); if (!q && matches.length === state.manifest.members.length) { els.results.classList.add('is-hidden'); els.search.setAttribute('aria-expanded', 'false'); return; } if (!matches.length) { var li0 = document.createElement('li'); li0.textContent = 'No members match.'; li0.setAttribute('aria-disabled', 'true'); els.results.appendChild(li0); } else { for (var i = 0; i < matches.length; i++) { var m = matches[i]; var li = document.createElement('li'); li.setAttribute('role', 'option'); li.setAttribute('data-id', m.id); li.textContent = m.n + ' (' + m.p + '-' + m.s + ', ' + (m.c === 'H' ? 'House' : 'Senate') + ')'; li.addEventListener('click', onResultClick); els.results.appendChild(li); } } els.results.classList.remove('is-hidden'); els.search.setAttribute('aria-expanded', 'true'); } function onResultClick(e) { var id = e.currentTarget.getAttribute('data-id'); if (!isValidId(id)) return; els.results.classList.add('is-hidden'); els.search.setAttribute('aria-expanded', 'false'); selectMember(id); } function isValidId(id) { return typeof id === 'string' && ID_RE.test(id) && Object.prototype.hasOwnProperty.call(state.membersById, id); } function selectMember(id) { if (!isValidId(id)) return; var version = state.manifest.version || ''; var url = state.BASE + 'members/' + encodeURIComponent(id) + '.json?v=' + encodeURIComponent(version); fetch(url, { credentials: 'omit' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function (member) { state.currentId = id; state.currentMember = member; renderMember(member); try { localStorage.setItem(LS_KEY, id); } catch (_) {} if (!state.skipPush) { var u = buildShareUrl(id); history.pushState({ id: id }, '', u); } state.skipPush = false; document.title = member.name + ' — 119th Congress Voting Dashboard'; }) .catch(function (e) { showError('Failed to load member ' + id + ': ' + e.message); }); } function showError(msg) { els.summary.replaceChildren(); var p = document.createElement('p'); p.style.color = '#c0392b'; p.textContent = msg; els.summary.appendChild(p); } function renderMember(member) { renderSummary(member); renderNote(member); renderKPIs(member); updateCharts(member); renderTable(member.metrics.rows || []); } function renderSummary(m) { els.summary.replaceChildren(); var h = document.createElement('h2'); h.textContent = m.name; h.style.margin = '0 0 6px'; els.summary.appendChild(h); var sub = document.createElement('div'); sub.style.color = 'var(--ps-text-muted)'; sub.style.fontSize = '0.92rem'; var chamberLabel = m.chamber === 'house' ? 'House' : 'Senate'; var distSuffix = (m.district && m.chamber === 'house') ? '-' + m.district : ''; var endDate = m.served_to || 'present'; sub.textContent = chamberLabel + ' · ' + m.party + '-' + m.state + distSuffix + ' · served ' + (m.served_from || '?') + ' – ' + endDate; els.summary.appendChild(sub); } var TERRITORY_NAMES = { AS: 'American Samoa', DC: 'the District of Columbia', GU: 'Guam', MP: 'the Northern Mariana Islands', PR: 'Puerto Rico', VI: 'the U.S. Virgin Islands' }; function renderNote(m) { var partial = m.served_partial === true; var noVotes = !m.metrics || m.metrics.total === 0; var isDelegate = m.is_delegate === true; if (isDelegate) { var terr = TERRITORY_NAMES[m.state] || m.state; els.note.textContent = 'Note: This member is the non-voting delegate from ' + terr + '. House delegates may vote in committees and on amendments in the Committee of the Whole, ' + 'but cannot vote on final passage on the House floor. Their low participation rate is structural, not absenteeism.'; els.note.classList.remove('is-hidden'); } else if (partial || noVotes) { var endDate = m.served_to || 'present'; els.note.textContent = 'This member did not cast roll-call votes during the period analyzed (served ' + (m.served_from || '?') + ' – ' + endDate + '). The dashboards below reflect that absence.'; els.note.classList.remove('is-hidden'); } else { els.note.textContent = ''; els.note.classList.add('is-hidden'); } } function pct(n, d) { if (!d) return '0%'; return ((n / d) * 100).toFixed(1) + '%'; } function renderKPIs(m) { var x = m.metrics || {}; var voting = x.voting || 0; var kpis = [ ['Total Votes', String(x.total || 0)], ['Yea / Nay', (x.yeas || 0) + ' / ' + (x.nays || 0)], ['Participation', pct(voting, x.total || 0)], ['Voted With GOP', (x.voted_with_gop || 0) + ' (' + pct(x.voted_with_gop || 0, voting) + ')'], ['Voted With Dem', (x.voted_with_dem || 0) + ' (' + pct(x.voted_with_dem || 0, voting) + ')'], ['Voted Against GOP', (x.voted_against_gop || 0) + ' (' + pct(x.voted_against_gop || 0, voting) + ')'], ['Voted Against Dem', (x.voted_against_dem || 0) + ' (' + pct(x.voted_against_dem || 0, voting) + ')'], ['Lone Wolf Votes', String(x.lone_wolf || 0)] ]; var cards = els.kpis.querySelectorAll('.kpi-card'); for (var i = 0; i < cards.length; i++) { var label = cards[i].querySelector('.kpi-label'); var value = cards[i].querySelector('.kpi-value'); if (i < kpis.length) { label.textContent = kpis[i][0]; value.textContent = kpis[i][1]; } else { label.textContent = ''; value.textContent = ''; } } } function buildCharts() { if (typeof Chart === 'undefined') return; var common = { responsive: true, maintainAspectRatio: false }; charts.voteDist = new Chart(document.getElementById('chart-vote-dist'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Member vote', data: [], backgroundColor: ['#27ae60', '#c0392b', '#f39c12', '#7f8c8d'] }] }, options: Object.assign({}, common, { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }) }); charts.alignment = new Chart(document.getElementById('chart-alignment'), { type: 'doughnut', data: { labels: ALIGN_KEYS.slice(), datasets: [{ data: [0, 0, 0, 0], backgroundColor: ALIGN_COLORS.slice() }] }, options: Object.assign({}, common, { plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: function (ctx) { var dataArr = ctx.dataset.data || []; var sum = 0; for (var i = 0; i < dataArr.length; i++) sum += Number(dataArr[i]) || 0; var v = Number(ctx.parsed) || 0; var p = sum > 0 ? ((v / sum) * 100).toFixed(1) + '%' : '0%'; return String(ctx.label) + ': ' + v + ' (' + p + ')'; } } } } }) }); charts.blocked = new Chart(document.getElementById('chart-blocked'), { type: 'bar', data: { labels: ['Dem-Backed', 'GOP-Backed'], datasets: [{ label: 'Blocked', data: [0, 0], backgroundColor: [COLORS.helpedD, COLORS.helpedR] }] }, options: Object.assign({}, common, { indexAxis: 'y', plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true } } }) }); charts.alignmentTime = new Chart(document.getElementById('chart-alignment-time'), { type: 'line', data: { labels: [], datasets: ALIGN_KEYS.map(function (k, i) { return { label: k, data: [], borderColor: ALIGN_COLORS[i], backgroundColor: ALIGN_COLORS[i], tension: 0.2, fill: false }; }) }, options: Object.assign({}, common, { plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } }) }); charts.withAgainst = new Chart(document.getElementById('chart-with-against'), { type: 'bar', data: { labels: ['GOP majority', 'Dem majority'], datasets: [ { label: 'Voted With', data: [0, 0], backgroundColor: COLORS.green }, { label: 'Voted Against', data: [0, 0], backgroundColor: COLORS.red } ] }, options: Object.assign({}, common, { plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } }) }); } function updateCharts(member) { if (typeof Chart === 'undefined') return; var x = member.metrics || {}; var mv = x.member || {}; var mvKeys = ['Yea', 'Nay', 'Present', 'Not Voting']; charts.voteDist.data.labels = mvKeys; charts.voteDist.data.datasets[0].data = mvKeys.map(function (k) { return Number(mv[k]) || 0; }); charts.voteDist.update('none'); var al = x.alignment || {}; charts.alignment.data.datasets[0].data = ALIGN_KEYS.map(function (k) { return Number(al[k]) || 0; }); charts.alignment.update('none'); charts.blocked.data.datasets[0].data = [Number(x.blocked_dem_count) || 0, Number(x.blocked_rep_count) || 0]; charts.blocked.update('none'); var months = Array.isArray(x.months) ? x.months : []; var monthly = x.monthly || {}; charts.alignmentTime.data.labels = months.slice(); for (var i = 0; i < ALIGN_KEYS.length; i++) { var arr = monthly[ALIGN_KEYS[i]]; charts.alignmentTime.data.datasets[i].data = Array.isArray(arr) ? arr.slice() : months.map(function () { return 0; }); } charts.alignmentTime.update('none'); charts.withAgainst.data.datasets[0].data = [Number(x.voted_with_gop) || 0, Number(x.voted_with_dem) || 0]; charts.withAgainst.data.datasets[1].data = [Number(x.voted_against_gop) || 0, Number(x.voted_against_dem) || 0]; charts.withAgainst.update('none'); } // ---- Table ---- var COL_DEFS = [ { key: 'y', numeric: true }, { key: 'r', numeric: true }, { key: 'd', numeric: false }, { key: 'ln', numeric: false }, { key: 'q', numeric: false }, { key: 'ds', numeric: false }, { key: 'rs', numeric: false }, { key: 'm', numeric: false }, { key: 'ry_rn', numeric: false }, { key: 'dy_dn', numeric: false }, { key: 'a', numeric: false }, { key: 'b', numeric: false } ]; function initSortHeaders() { var ths = els.tableHead.querySelectorAll('th'); for (var i = 0; i < ths.length; i++) { ths[i].style.cursor = 'pointer'; ths[i].setAttribute('data-col-index', String(i)); ths[i].addEventListener('click', onSortClick); } } function onSortClick(e) { var idx = Number(e.currentTarget.getAttribute('data-col-index')); if (isNaN(idx)) return; if (state.sortCol === idx) { state.sortAsc = !state.sortAsc; } else { state.sortCol = idx; state.sortAsc = true; } updateSortIndicators(); if (state.currentMember) renderTable(state.currentMember.metrics.rows || []); } function updateSortIndicators() { var ths = els.tableHead.querySelectorAll('th'); for (var i = 0; i < ths.length; i++) { var base = ths[i].textContent.replace(/[▲▼]\s*$/, '').trim(); ths[i].textContent = base + (i === state.sortCol ? (state.sortAsc ? ' ▲' : ' ▼') : ''); } } function renderTable(rows) { var sorted = rows.slice(); if (state.sortCol != null && state.sortCol >= 0 && state.sortCol < COL_DEFS.length) { var col = COL_DEFS[state.sortCol]; var dir = state.sortAsc ? 1 : -1; sorted.sort(function (a, b) { var va, vb; if (col.key === 'ry_rn') { va = (a.ry || 0) - (a.rn || 0); vb = (b.ry || 0) - (b.rn || 0); } else if (col.key === 'dy_dn') { va = (a.dy || 0) - (a.dn || 0); vb = (b.dy || 0) - (b.dn || 0); } else { va = a[col.key]; vb = b[col.key]; } if (col.numeric) { va = Number(va) || 0; vb = Number(vb) || 0; return (va - vb) * dir; } va = String(va == null ? '' : va); vb = String(vb == null ? '' : vb); return va.localeCompare(vb) * dir; }); } els.tableBody.replaceChildren(); var frag = document.createDocumentFragment(); for (var i = 0; i < sorted.length; i++) { var r = sorted[i]; var tr = document.createElement('tr'); appendCell(tr, r.y); appendCell(tr, r.r); appendCell(tr, r.d); appendCell(tr, r.ln); appendCell(tr, r.q); appendCell(tr, r.ds); appendCell(tr, r.rs); appendCell(tr, r.m); appendCell(tr, (r.ry || 0) + ' / ' + (r.rn || 0)); appendCell(tr, (r.dy || 0) + ' / ' + (r.dn || 0)); appendCell(tr, r.a); appendCell(tr, r.b); frag.appendChild(tr); } els.tableBody.appendChild(frag); } function appendCell(tr, val) { var td = document.createElement('td'); td.textContent = val == null ? '' : String(val); tr.appendChild(td); } // ---- URL / history ---- function buildShareUrl(id) { var params = new URLSearchParams(); if (id) params.set('id', id); var q = (els.search.value || '').trim(); if (q) params.set('q', q); var parties = getCheckedValues(els.filterParty); if (parties.length > 0 && parties.length < 3) params.set('p', parties.join(',')); var chambers = getCheckedValues(els.filterChamber); if (chambers.length > 0 && chambers.length < 2) params.set('c', chambers.join(',')); var statesSel = getSelectedStates(); if (statesSel.length > 0 && statesSel.length < els.filterState.options.length) { params.set('s', statesSel.join(',')); } var s = params.toString(); return s ? ('?' + s) : window.location.pathname; } function debounceUrlReplace() { if (state.urlReplaceTimer) clearTimeout(state.urlReplaceTimer); // Debounce to avoid hammering history during keystrokes. state.urlReplaceTimer = setTimeout(function () { var u = buildShareUrl(state.currentId); history.replaceState({ id: state.currentId }, '', u); }, 250); } function restoreFiltersFromURL() { var params = new URLSearchParams(window.location.search); var p = params.get('p'); var c = params.get('c'); var s = params.get('s'); var q = params.get('q'); if (p) { var allowed = p.split(','); els.filterParty.querySelectorAll('input[type=checkbox]').forEach(function (i) { i.checked = allowed.indexOf(i.value) >= 0; }); } if (c) { var allowedC = c.split(','); els.filterChamber.querySelectorAll('input[type=checkbox]').forEach(function (i) { i.checked = allowedC.indexOf(i.value) >= 0; }); } if (s) { var allowedS = s.split(','); for (var i = 0; i < els.filterState.options.length; i++) { els.filterState.options[i].selected = allowedS.indexOf(els.filterState.options[i].value) >= 0; } } if (q) els.search.value = q; } function onPopState(e) { var params = new URLSearchParams(window.location.search); var id = params.get('id'); if (id && isValidId(id)) { state.skipPush = true; selectMember(id); } } function initialSelect() { var params = new URLSearchParams(window.location.search); var urlId = params.get('id'); if (urlId && isValidId(urlId)) { state.skipPush = true; selectMember(urlId); return; } var stored = null; try { stored = localStorage.getItem(LS_KEY); } catch (_) {} if (stored && isValidId(stored)) { state.skipPush = true; selectMember(stored); return; } // No selection — leave summary empty with a hint. els.summary.replaceChildren(); var p = document.createElement('p'); p.style.color = 'var(--ps-text-muted)'; p.textContent = 'Pick a member to begin — type a name, set of initials (e.g., AOC), or use the filters at left.'; els.summary.appendChild(p); } })();