| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- #!/usr/bin/env python3
- """Parameterized builder: parse cached clerk.house.gov XMLs for a given member
- and emit a self-contained dashboard HTML.
- Usage: python3 build_member.py <bioguide_id> "<Full Name>" <party R|D|I> <output_filename>
- """
- import sys, os, json, glob
- import xml.etree.ElementTree as ET
- from collections import Counter, defaultdict
- CACHE_DIR = "/home/user/polisci/vote_cache"
- OUT_DIR = "/home/user/polisci/results"
- MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
- def parse_xml(path, year, roll, bioguide):
- try:
- with open(path, "rb") as f:
- root = ET.fromstring(f.read())
- except Exception:
- return None
- meta = root.find("vote-metadata")
- if meta is None: return None
- def t(tag):
- el = meta.find(tag)
- return (el.text or "").strip() if el is not None else ""
- info = {
- "year": year, "roll": roll,
- "date": t("action-date"),
- "question": t("vote-question"),
- "result": t("vote-result"),
- "legis_num": t("legis-num"),
- "desc": t("vote-desc"),
- }
- party_totals = {}
- for pt in meta.findall("vote-totals/totals-by-party"):
- party = pt.findtext("party","").strip()
- party_totals[party] = {
- "yea": int(pt.findtext("yea-total","0") or 0),
- "nay": int(pt.findtext("nay-total","0") or 0),
- }
- info["R"] = party_totals.get("Republican", {"yea":0,"nay":0})
- info["D"] = party_totals.get("Democratic", {"yea":0,"nay":0})
- member_vote = None
- for rv in root.iter("recorded-vote"):
- leg = rv.find("legislator")
- if leg is not None and leg.get("name-id") == bioguide:
- v = rv.find("vote")
- member_vote = (v.text or "").strip() if v is not None else None
- break
- info["member"] = member_vote
- return info
- def classify(v):
- r_yea, r_nay = v["R"]["yea"], v["R"]["nay"]
- d_yea, d_nay = v["D"]["yea"], v["D"]["nay"]
- r_pos = "Yea" if r_yea > r_nay else ("Nay" if r_nay > r_yea else "Split")
- d_pos = "Yea" if d_yea > d_nay else ("Nay" if d_nay > d_yea else "Split")
- m = v["member"]
- if m not in ("Yea","Nay","Aye","No"):
- return ("N/A: " + (m or "absent"), None, r_pos, d_pos)
- m_norm = "Yea" if m in ("Yea","Aye") else "Nay"
- helped_r = (r_pos != "Split" and m_norm == r_pos)
- helped_d = (d_pos != "Split" and m_norm == d_pos)
- if helped_r and helped_d: align = "Helped Both"
- elif helped_r: align = "Helped Republicans"
- elif helped_d: align = "Helped Democrats"
- else: align = "Helped Neither"
- result = v["result"].lower()
- measure_failed = "fail" in result or "reject" in result or "not agreed" in result or "not passed" in result
- blocked = None
- if d_pos == "Yea" and measure_failed and m_norm == "Nay" and r_pos != "Yea":
- blocked = "Democrat"
- if r_pos == "Yea" and measure_failed and m_norm == "Nay" and d_pos != "Yea":
- blocked = "Republican"
- return (align, blocked, r_pos, d_pos)
- def gather(bioguide, member_party):
- votes = []
- for path in sorted(glob.glob(f"{CACHE_DIR}/*.xml")):
- base = os.path.basename(path).replace(".xml","")
- try:
- year, roll = base.split("_")
- year, roll = int(year), int(roll)
- except Exception:
- continue
- v = parse_xml(path, year, roll, bioguide)
- if not v: continue
- align, blocked, r_pos, d_pos = classify(v)
- v["alignment"] = align
- v["blocked"] = blocked
- v["r_pos"] = r_pos
- v["d_pos"] = d_pos
- votes.append(v)
- return votes
- def aggregate(votes, member_party):
- align_counts = Counter(v["alignment"] for v in votes)
- member_counts = Counter(v["member"] for v in votes)
- blocked_counts = Counter(v["blocked"] for v in votes if v["blocked"])
- month_align = defaultdict(Counter)
- for v in votes:
- try:
- d,m,y = v["date"].split("-")
- mk = f"{y}-{MONTHS.index(m)+1:02d}"
- month_align[mk][v["alignment"]] += 1
- except Exception:
- pass
- months_sorted = sorted(month_align.keys())
- align_labels = ["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"]
- monthly = {l: [month_align[m].get(l,0) for m in months_sorted] for l in align_labels}
- def row(v):
- return {"y":v["year"],"r":v["roll"],"d":v["date"],"ln":v["legis_num"],
- "q":v["question"],"ds":v["desc"][:90],"rs":v["result"],
- "m":v["member"],
- "ry":v["R"]["yea"],"rn":v["R"]["nay"],
- "dy":v["D"]["yea"],"dn":v["D"]["nay"],
- "a":v["alignment"],"b":v["blocked"] or ""}
- rows = [row(v) for v in votes]
- total = len(votes)
- voting = sum(1 for v in votes if v["member"] in ("Yea","Nay","Aye","No"))
- yeas = sum(1 for v in votes if v["member"] in ("Yea","Aye"))
- nays = sum(1 for v in votes if v["member"] in ("Nay","No"))
- nv = sum(1 for v in votes if v["member"] == "Not Voting")
- present = sum(1 for v in votes if v["member"] == "Present")
- va_gop = vw_gop = va_dem = vw_dem = 0
- for v in votes:
- if v["member"] not in ("Yea","Nay","Aye","No"): continue
- m_norm = "Yea" if v["member"] in ("Yea","Aye") else "Nay"
- r_pos = v["r_pos"]; d_pos = v["d_pos"]
- if r_pos != "Split":
- if m_norm == r_pos: vw_gop += 1
- else: va_gop += 1
- if d_pos != "Split":
- if m_norm == d_pos: vw_dem += 1
- else: va_dem += 1
- # Lone-wolf: defected from OWN party majority with <=5 fellow defectors
- own = member_party
- lone_wolf = 0
- for v in votes:
- if v["member"] not in ("Yea","Nay","Aye","No"): continue
- m_norm = "Yea" if v["member"] in ("Yea","Aye") else "Nay"
- own_pos = v["r_pos"] if own == "R" else v["d_pos"]
- if own_pos == "Split" or m_norm == own_pos: continue
- own_totals = v["R"] if own == "R" else v["D"]
- defectors = own_totals["nay"] if own_pos == "Yea" else own_totals["yea"]
- if defectors <= 5: lone_wolf += 1
- blocked_dem = sum(1 for v in votes if v["blocked"] == "Democrat")
- blocked_rep = sum(1 for v in votes if v["blocked"] == "Republican")
- return {
- "total":total,"voting":voting,"yeas":yeas,"nays":nays,"nv":nv,"present":present,
- "alignment":dict(align_counts),"member":dict(member_counts),
- "blocked":dict(blocked_counts),"months":months_sorted,"monthly":monthly,
- "rows":rows,
- "blocked_dem_count":blocked_dem,"blocked_rep_count":blocked_rep,
- "lone_wolf":lone_wolf,
- "voted_against_gop":va_gop,"voted_against_dem":va_dem,
- "voted_with_gop":vw_gop,"voted_with_dem":vw_dem,
- "voted_against_own":(va_gop if own=="R" else va_dem),
- "voted_with_own":(vw_gop if own=="R" else vw_dem),
- }
- HTML_TEMPLATE = r"""<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>__NAME__ - 119th Congress Voting Dashboard</title>
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
- <style>
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;margin:0;padding:24px;background:#f5f5f7;color:#1d1d1f;}
- h1{margin:0 0 4px;font-size:28px;} .sub{color:#6e6e73;margin-bottom:24px;}
- .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:24px;}
- .card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);cursor:grab;transition:box-shadow .15s,transform .15s;}
- .card:active{cursor:grabbing;} .card.sortable-ghost{opacity:.4;}
- .card.sortable-chosen{box-shadow:0 4px 16px rgba(0,0,0,.15);transform:scale(1.02);}
- .card.sortable-drag{box-shadow:0 8px 24px rgba(0,0,0,.2);}
- .kpi{font-size:32px;font-weight:700;margin:6px 0 2px;} .pct{font-size:14px;font-weight:500;color:#6e6e73;margin-left:4px;}
- .kpi-label{color:#6e6e73;font-size:13px;text-transform:uppercase;letter-spacing:.5px;}
- .chart-row{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px;}
- @media (max-width:900px){.chart-row{grid-template-columns:1fr;}}
- .chart-card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);min-width:0;}
- .chart-box{position:relative;height:300px;width:100%;}
- h2{font-size:18px;margin:0 0 12px;}
- table{width:100%;border-collapse:collapse;font-size:13px;}
- th,td{padding:6px 8px;text-align:left;border-bottom:1px solid #e8e8ed;}
- th{background:#fafafa;position:sticky;top:0;cursor:pointer;user-select:none;}
- th:hover{background:#f0f0f5;}
- .table-wrap{max-height:600px;overflow:auto;border:1px solid #e8e8ed;border-radius:8px;}
- .filter-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;}
- .filter-bar input,.filter-bar select{padding:8px 12px;border:1px solid #d2d2d7;border-radius:8px;font-size:14px;}
- .filter-bar input{flex:1;min-width:200px;}
- .badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;}
- .b-rep{background:#fde2e2;color:#a30000;} .b-dem{background:#dde7fa;color:#1644a0;}
- .b-both{background:#e0f0e0;color:#1e6b1e;} .b-neither{background:#f0e0f0;color:#6b1e6b;}
- .b-na{background:#eee;color:#666;}
- .v-yea{color:#1e6b1e;font-weight:600;} .v-nay{color:#a30000;font-weight:600;}
- .v-nv,.v-present{color:#888;}
- .note{font-size:12px;color:#6e6e73;line-height:1.5;margin-top:12px;}
- .drag-hint{font-size:11px;color:#6e6e73;margin-bottom:8px;}
- .drag-hint button{margin-left:8px;padding:2px 8px;font-size:11px;border:1px solid #d2d2d7;border-radius:4px;background:#fff;cursor:pointer;}
- .party-pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;margin-left:8px;color:#fff;}
- .footer{text-align:center;color:#6e6e73;font-size:12px;margin-top:32px;}
- .member-note{background:#fff8e1;border-left:4px solid #f5b400;padding:12px 16px;border-radius:6px;margin:0 0 20px;font-size:14px;line-height:1.5;color:#5a4500;}
- .member-note strong{color:#3d2f00;}
- </style>
- </head>
- <body>
- <h1>__NAME__<span class="party-pill" style="background:__PARTY_COLOR__">__PARTY__</span></h1>
- <div class="sub">Bioguide __BIOGUIDE__ · House roll-call votes, 119th Congress (Jan 2025 – present) · Source: clerk.house.gov</div>
- __MEMBER_NOTE__
- <div class="drag-hint">↕ Drag any card to rearrange. <button id="resetOrder">Reset order</button></div>
- <div class="grid" id="cardGrid">
- <div class="card" data-id="participation"><div class="kpi-label">Roll Calls / Participation</div><div class="kpi">__VOTING__ <span style="font-size:18px;color:#6e6e73">/ __TOTAL__</span> <span class="pct">__VOTING_PCT__%</span></div><div class="note">119th Congress · Yea/Aye: __YEAS__ · Nay/No: __NAYS__ · Not Voting: __NV__ · Present: __PRESENT__</div></div>
- <div class="card" data-id="va_gop"><div class="kpi-label">Voted Against GOP Majority</div><div class="kpi" style="color:#a30000">__VA_GOP__ <span class="pct">__VA_GOP_PCT__%</span></div><div class="note">of __PARTISAN_R__ votes where R majority took a side</div></div>
- <div class="card" data-id="va_dem"><div class="kpi-label">Voted Against Dem Majority</div><div class="kpi" style="color:#1644a0">__VA_DEM__ <span class="pct">__VA_DEM_PCT__%</span></div><div class="note">of __PARTISAN_D__ votes where D majority took a side</div></div>
- <div class="card" data-id="blk_dem"><div class="kpi-label">Blocked Dem-Backed</div><div class="kpi" style="color:#1644a0">__BLK_DEM__ <span class="pct">__BLK_DEM_PCT__%</span></div><div class="note">Member Nay + D wanted Yea + measure failed</div></div>
- <div class="card" data-id="blk_rep"><div class="kpi-label">Blocked GOP-Backed</div><div class="kpi" style="color:#a30000">__BLK_REP__ <span class="pct">__BLK_REP_PCT__%</span></div><div class="note">Member Nay + R wanted Yea + measure failed</div></div>
- <div class="card" data-id="lone"><div class="kpi-label">Lone Wolf Defections</div><div class="kpi">__LONE__ <span class="pct">__LONE_PCT__%</span></div><div class="note">Defected from own (__PARTY__) party majority w/ ≤5 fellow defectors</div></div>
- </div>
- <div class="chart-row">
- <div class="chart-card"><h2>Alignment Classification</h2><div class="chart-box"><canvas id="alignChart"></canvas></div></div>
- <div class="chart-card"><h2>Vote Distribution</h2><div class="chart-box"><canvas id="voteChart"></canvas></div></div>
- </div>
- <div class="chart-row">
- <div class="chart-card"><h2>Voted With vs. Against — by Party Majority</h2><div class="chart-box"><canvas id="partyChart"></canvas></div></div>
- <div class="chart-card"><h2>Blocking Wins</h2><div class="chart-box"><canvas id="blockChart"></canvas></div></div>
- </div>
- <div class="chart-row">
- <div class="chart-card" style="grid-column:1 / -1"><h2>Alignment Over Time (monthly)</h2><div class="chart-box" style="height:320px"><canvas id="trendChart"></canvas></div></div>
- </div>
- <div class="chart-row">
- <div class="chart-card" style="grid-column:1 / -1">
- <h2>All Votes (filterable)</h2>
- <div class="filter-bar">
- <input id="search" placeholder="Search bill/description…">
- <select id="alignFilter"><option value="">All alignments</option><option>Helped Republicans</option><option>Helped Democrats</option><option>Helped Both</option><option>Helped Neither</option></select>
- <select id="blockFilter"><option value="">All votes</option><option value="Democrat">Blocked Dem-backed</option><option value="Republican">Blocked GOP-backed</option></select>
- <select id="memberFilter"><option value="">Any vote</option><option>Yea</option><option>Aye</option><option>Nay</option><option>No</option><option>Not Voting</option><option>Present</option></select>
- </div>
- <div class="table-wrap"><table id="voteTable">
- <thead><tr><th data-k="y">Yr</th><th data-k="r">#</th><th data-k="d">Date</th><th data-k="ln">Bill</th><th data-k="q">Question</th><th data-k="ds">Description</th><th data-k="rs">Result</th><th data-k="m">Vote</th><th data-k="ry">R Yea</th><th data-k="rn">R Nay</th><th data-k="dy">D Yea</th><th data-k="dn">D Nay</th><th data-k="a">Alignment</th><th data-k="b">Blocked</th></tr></thead>
- <tbody id="voteBody"></tbody></table></div>
- <div class="note">Click headers to sort. Showing <span id="row-count">0</span> rows.</div>
- </div>
- </div>
- <div class="footer">Source: clerk.house.gov XML rollcalls · Built 2026-05-23 · See DOCUMENTATION.md for methodology</div>
- <script>
- const DATA = __DATA__;
- const MEMBER_PARTY = "__PARTY__";
- const CARD_ORDER_KEY = 'dashCardOrder.__BIOGUIDE__';
- const grid = document.getElementById('cardGrid');
- function applyOrder(order){if(!order)return;const byId={};Array.from(grid.children).forEach(c=>byId[c.dataset.id]=c);order.forEach(id=>{if(byId[id])grid.appendChild(byId[id]);});}
- try{applyOrder(JSON.parse(localStorage.getItem(CARD_ORDER_KEY)));}catch(e){}
- Sortable.create(grid,{animation:180,ghostClass:'sortable-ghost',chosenClass:'sortable-chosen',dragClass:'sortable-drag',
- onEnd(){localStorage.setItem(CARD_ORDER_KEY,JSON.stringify(Array.from(grid.children).map(c=>c.dataset.id)));}});
- document.getElementById('resetOrder').addEventListener('click',()=>{localStorage.removeItem(CARD_ORDER_KEY);location.reload();});
- const ALIGN_COLORS={"Helped Republicans":"#d93b3b","Helped Democrats":"#3b6ed9","Helped Both":"#3ba85a","Helped Neither":"#a13bd9","N/A: Not Voting":"#999","N/A: Present":"#bbb"};
- const alignOrder=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither","N/A: Not Voting","N/A: Present"];
- const alignLabels=alignOrder.filter(k=>DATA.alignment[k]);
- const alignData=alignLabels.map(k=>DATA.alignment[k]);
- const alignColors=alignLabels.map(k=>ALIGN_COLORS[k]||"#888");
- const alignTotal=alignData.reduce((a,b)=>a+b,0);
- const alignLabelsPct=alignLabels.map((l,i)=>`${l} — ${alignData[i]} (${(alignData[i]/alignTotal*100).toFixed(1)}%)`);
- new Chart(document.getElementById('alignChart'),{type:'doughnut',data:{labels:alignLabelsPct,datasets:[{data:alignData,backgroundColor:alignColors}]},
- options:{plugins:{legend:{position:'right',labels:{boxWidth:14,font:{size:12}}},
- tooltip:{callbacks:{label:ctx=>`${alignLabels[ctx.dataIndex]}: ${ctx.parsed} (${(ctx.parsed/alignTotal*100).toFixed(1)}%)`}}},
- responsive:true,maintainAspectRatio:false}});
- const voteOrder=["Yea","Aye","Nay","No","Not Voting","Present"];
- const voteLabels=voteOrder.filter(k=>DATA.member[k]);
- const voteData=voteLabels.map(k=>DATA.member[k]);
- const voteTotal=voteData.reduce((a,b)=>a+b,0);
- const barLabelPlugin={id:'barLabel',afterDatasetsDraw(chart){const {ctx}=chart;
- chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
- const v=ds.data[i],pct=(v/voteTotal*100).toFixed(1);
- ctx.fillStyle='#333';ctx.font='600 11px -apple-system, sans-serif';ctx.textAlign='center';
- ctx.fillText(`${v} (${pct}%)`,bar.x,bar.y-6);});});}};
- new Chart(document.getElementById('voteChart'),{type:'bar',
- data:{labels:voteLabels,datasets:[{label:'Count',data:voteData,
- backgroundColor:voteLabels.map(l=>l==='Yea'||l==='Aye'?'#1e8e3e':l==='Nay'||l==='No'?'#c5221f':'#888')}]},
- options:{plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.y} (${(ctx.parsed.y/voteTotal*100).toFixed(1)}%)`}}},
- responsive:true,maintainAspectRatio:false,layout:{padding:{top:20}},scales:{y:{beginAtZero:true}}},
- plugins:[barLabelPlugin]});
- const partyTotR=DATA.voted_with_gop+DATA.voted_against_gop;
- const partyTotD=DATA.voted_with_dem+DATA.voted_against_dem;
- new Chart(document.getElementById('partyChart'),{type:'bar',
- data:{labels:['GOP majority','Dem majority'],datasets:[
- {label:'Voted WITH',data:[DATA.voted_with_gop,DATA.voted_with_dem],backgroundColor:'#1e8e3e'},
- {label:'Voted AGAINST',data:[DATA.voted_against_gop,DATA.voted_against_dem],backgroundColor:'#c5221f'}]},
- options:{responsive:true,maintainAspectRatio:false,
- plugins:{tooltip:{callbacks:{label:ctx=>{const tot=ctx.dataIndex===0?partyTotR:partyTotD;return `${ctx.dataset.label}: ${ctx.parsed.y} (${(ctx.parsed.y/tot*100).toFixed(1)}%)`;}}}},
- scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}}},
- plugins:[{id:'stackPct',afterDatasetsDraw(chart){const {ctx}=chart;
- ctx.fillStyle='#fff';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='center';
- chart.data.datasets.forEach((ds,di)=>{chart.getDatasetMeta(di).data.forEach((bar,i)=>{
- const tot=i===0?partyTotR:partyTotD,v=ds.data[i];
- if(v>0)ctx.fillText(`${v} (${(v/tot*100).toFixed(1)}%)`,bar.x,(bar.y+bar.base)/2+4);});});}}]});
- const blkTot=DATA.blocked_dem_count+DATA.blocked_rep_count;
- new Chart(document.getElementById('blockChart'),{type:'bar',
- data:{labels:['Blocked Dem-backed','Blocked GOP-backed'],
- datasets:[{label:'Count',data:[DATA.blocked_dem_count,DATA.blocked_rep_count],backgroundColor:['#3b6ed9','#d93b3b']}]},
- options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,
- plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>`${ctx.parsed.x} (${blkTot?(ctx.parsed.x/blkTot*100).toFixed(1):'0.0'}%)`}}},
- scales:{x:{beginAtZero:true}}},
- plugins:[{id:'hbarLabel',afterDatasetsDraw(chart){const {ctx}=chart;
- ctx.fillStyle='#333';ctx.font='600 12px -apple-system, sans-serif';ctx.textAlign='left';ctx.textBaseline='middle';
- chart.getDatasetMeta(0).data.forEach((bar,i)=>{const v=chart.data.datasets[0].data[i];
- ctx.fillText(` ${v} (${blkTot?(v/blkTot*100).toFixed(1):'0.0'}%)`,bar.x,bar.y);});}}]});
- const trendLabels=DATA.months;
- const trendSets=["Helped Republicans","Helped Democrats","Helped Both","Helped Neither"].map(k=>({
- label:k,data:DATA.monthly[k],borderColor:ALIGN_COLORS[k],backgroundColor:ALIGN_COLORS[k]+'33',tension:.3,fill:false}));
- new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:trendLabels,datasets:trendSets},
- options:{responsive:true,maintainAspectRatio:false,
- scales:{y:{beginAtZero:true,title:{display:true,text:'Votes per month'}}},interaction:{mode:'index',intersect:false}}});
- let sortKey='r',sortDesc=true;
- function renderTable(){
- const q=document.getElementById('search').value.toLowerCase();
- const af=document.getElementById('alignFilter').value;
- const bf=document.getElementById('blockFilter').value;
- const mf=document.getElementById('memberFilter').value;
- let rows=DATA.rows.filter(r=>{
- if(q&&!((r.ln+' '+r.ds+' '+r.q).toLowerCase().includes(q)))return false;
- if(af&&r.a!==af)return false; if(bf&&r.b!==bf)return false; if(mf&&r.m!==mf)return false; return true;});
- rows.sort((a,b)=>{const av=a[sortKey],bv=b[sortKey];let cmp;
- if(typeof av==='number')cmp=av-bv;else cmp=String(av).localeCompare(String(bv));
- if(cmp===0&&sortKey!=='r')cmp=(a.y-b.y)||(a.r-b.r);return sortDesc?-cmp:cmp;});
- const tb=document.getElementById('voteBody');
- const aClass=a=>'badge '+({"Helped Republicans":"b-rep","Helped Democrats":"b-dem","Helped Both":"b-both","Helped Neither":"b-neither"}[a]||"b-na");
- const mClass=m=>'v-'+(m==='Yea'||m==='Aye'?'yea':m==='Nay'||m==='No'?'nay':m==='Not Voting'?'nv':'present');
- tb.innerHTML=rows.map(r=>`<tr><td>${r.y}</td><td>${r.r}</td><td>${r.d}</td><td>${r.ln||''}</td><td>${r.q||''}</td><td>${r.ds||''}</td><td>${r.rs}</td><td class="${mClass(r.m)}">${r.m||''}</td><td>${r.ry}</td><td>${r.rn}</td><td>${r.dy}</td><td>${r.dn}</td><td><span class="${aClass(r.a)}">${r.a}</span></td><td>${r.b?`<span class="badge ${r.b==='Democrat'?'b-dem':'b-rep'}">${r.b}</span>`:''}</td></tr>`).join('');
- document.getElementById('row-count').textContent=rows.length;
- }
- document.querySelectorAll('#voteTable th').forEach(th=>{th.addEventListener('click',()=>{const k=th.dataset.k;if(sortKey===k)sortDesc=!sortDesc;else{sortKey=k;sortDesc=true;}renderTable();});});
- ['search','alignFilter','blockFilter','memberFilter'].forEach(id=>document.getElementById(id).addEventListener('input',renderTable));
- renderTable();
- </script>
- </body></html>
- """
- def render(bioguide, name, party, out_name, note=None):
- votes = gather(bioguide, party)
- if not votes:
- print(f" WARN: no votes parsed for {name}", file=sys.stderr)
- data = aggregate(votes, party)
- def pct(n, d): return f"{(n/d*100):.1f}" if d else "0.0"
- partisan_r = data["voted_with_gop"] + data["voted_against_gop"]
- partisan_d = data["voted_with_dem"] + data["voted_against_dem"]
- color = "#d93b3b" if party == "R" else ("#3b6ed9" if party == "D" else "#888")
- note_html = f'<div class="member-note"><strong>Note:</strong> {note}</div>' if note else ''
- html = (HTML_TEMPLATE
- .replace("__MEMBER_NOTE__", note_html)
- .replace("__NAME__", name)
- .replace("__BIOGUIDE__", bioguide)
- .replace("__PARTY__", party)
- .replace("__PARTY_COLOR__", color)
- .replace("__TOTAL__", str(data["total"]))
- .replace("__VOTING__", str(data["voting"]))
- .replace("__VOTING_PCT__", pct(data["voting"], data["total"]))
- .replace("__YEAS__", str(data["yeas"]))
- .replace("__NAYS__", str(data["nays"]))
- .replace("__NV__", str(data["nv"]))
- .replace("__PRESENT__", str(data["present"]))
- .replace("__VA_GOP__", str(data["voted_against_gop"]))
- .replace("__VA_GOP_PCT__", pct(data["voted_against_gop"], partisan_r))
- .replace("__VA_DEM__", str(data["voted_against_dem"]))
- .replace("__VA_DEM_PCT__", pct(data["voted_against_dem"], partisan_d))
- .replace("__PARTISAN_R__", str(partisan_r))
- .replace("__PARTISAN_D__", str(partisan_d))
- .replace("__BLK_DEM__", str(data["blocked_dem_count"]))
- .replace("__BLK_DEM_PCT__", pct(data["blocked_dem_count"], data["total"]))
- .replace("__BLK_REP__", str(data["blocked_rep_count"]))
- .replace("__BLK_REP_PCT__", pct(data["blocked_rep_count"], data["total"]))
- .replace("__LONE__", str(data["lone_wolf"]))
- .replace("__LONE_PCT__", pct(data["lone_wolf"], data["voting"]))
- .replace("__DATA__", json.dumps(data))
- )
- out = os.path.join(OUT_DIR, out_name)
- with open(out, "w") as f: f.write(html)
- print(f" {name}: {data['voting']}/{data['total']} voted, "
- f"alignment R:{data['alignment'].get('Helped Republicans',0)} "
- f"D:{data['alignment'].get('Helped Democrats',0)} "
- f"Both:{data['alignment'].get('Helped Both',0)} "
- f"Neither:{data['alignment'].get('Helped Neither',0)} → {out}")
- if __name__ == "__main__":
- os.makedirs(OUT_DIR, exist_ok=True)
- if len(sys.argv) >= 5:
- render(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
- else:
- # Roster entries: (bioguide, name, party, output_filename, optional_note)
- # See DOCUMENTATION.md §3.1 for when to add a note.
- MTG_NOTE = ("Rep. Greene publicly announced her resignation from the House "
- "in late 2025, effective January 5, 2026. This explains her "
- "substantially lower participation count (325 of 553) versus "
- "other members analyzed. Votes after her departure date are "
- "necessarily recorded as Not Voting in clerk.house.gov data.")
- ROSTER = [
- ("K000389", "Ro Khanna", "D", "RoKhanna119.html"),
- ("O000172", "Alexandria Ocasio-Cortez", "D", "AlexandriaOcasioCortez119.html"),
- ("O000173", "Ilhan Omar", "D", "IlhanOmar119.html"),
- ("G000596", "Marjorie Taylor Greene", "R", "MarjorieTaylorGreene119.html", MTG_NOTE),
- ("J000289", "Jim Jordan", "R", "JimJordan119.html"),
- ("D000032", "Byron Donalds", "R", "ByronDonalds119.html"),
- ]
- for entry in ROSTER:
- bg, nm, pt, fn = entry[:4]
- note = entry[4] if len(entry) > 4 else None
- render(bg, nm, pt, fn, note)
|