Skip to content

lszoszk/hrc-voting

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OHCHR Voting Records — CHR · HRC

Live dashboard: https://lszoszk.github.io/hrc-voting/

DOI

Complete harvest of the Voting collection of the OHCHR search library (https://searchlibrary.ohchr.org/search?c=Voting) — voting records of the Commission on Human Rights (CHR, 1946–2006) and the Human Rights Council (HRC, 2006–present) — parsed into dashboard-ready CSVs and an interactive, zero-build static dashboard.

Coverage: 6,346 resolutions / 80,159 individual country roll-call votes (1946–2026). Record count matches the collection total exactly (verified by three independent harvest methods).

Layout

scripts/harvest.py             # download all MARCXML (per-year partitioning)
scripts/parse_marcxml.py       # MARCXML -> resolutions.csv + votes_long.csv
scripts/build_dashboard_data.py# CSVs -> dashboard/data.js (compact payload)
data/raw/                      # harvested MARCXML, one file per year (git-ignored)
data/csv/resolutions.csv       # one row per resolution (metadata + vote totals)
data/csv/votes_long.csv        # one row per (resolution, country) roll-call vote
dashboard/index.html           # self-contained static dashboard (open directly)
dashboard/data.js              # embedded data payload (~970 KB)

Dashboard

dashboard/index.html (+ data.js) is a self-contained static page (no build step, no CDN — works offline and on GitHub Pages). Open it directly or serve the folder. For sharing by e-mail, use dashboard/OHCHR_voting_dashboard.html — a single ~1.1 MB file with the data inlined (regenerate by re-running the inline step in the audit notes, or just re-copy after build_dashboard_data.py). scripts/smoke_test.py loads the page from file:// in headless Chromium (light

  • dark), clicks through every tab and key interaction, and fails on any console error — run it before sending the prototype onward. Five views over the ~1,705 recorded (roll-call) votes:
  • Overview — volume over time (CHR→HRC), how contested votes were, most divided votes.
  • Country profile — per-state yearly Yes/Abstain/No composition chart (100% columns + votes-per-year strip + dashed membership-gap markers), closest / furthest 30 voting partners (scrollable lists, coloured by UN group), a world choropleth of voting agreement with the selected state (same blue/red encoding as the blocs matrix; world.js generated by scripts/build_world_map.py from Natural Earth 110m + centroid dots for small states), and the full sortable vote table. All resolution tables across the app carry a Subject column (the catalogued OHCHR tag) so you can see what a resolution is about without clicking.
  • Topics — browse by OHCHR's own catalogued subject tags (agenda_subject, 991$d), split into thematic vs country-situation. The chip row shows the most frequent tags (expandable to all); the search box autocompletes subject tags after 2 letters (ranked by vote count, Enter picks the top hit) and free text also searches titles; results show a "related subject tags" row for one-click refinement. For any topic: the matching resolutions, a support-over-time trend (yearly average Yes-share from official totals, with a least-squares rising/falling note in pp per decade), and how every state voted (Yes/Abstain/No share, ranked by support).
  • Blocs & alignment — an alignment map (reference-pole scatter: each state placed by its % agreement with two anchor states you pick, default USA → vs China ↑, so position is directly interpretable), a voting-coincidence matrix (blue = alike, red = apart) with greedy clustering, regional-group cohesion (Hix–Noury–Roland Agreement Index per UN group) and a most-often-outvoted ranking (share of a state's votes that differ from the whole-chamber majority position). Country sets: 30 most active, each UN regional group, P5 + key states, plus computed 30 most agreeing / 30 most disagreeing states; filter by year range. Methods are documented in the Methodology tab.
  • Methodology — explains, with live numbers pulled from meta.coverage, why the other three views operate on ~1,705 resolutions instead of the full 6,346 (only recorded/roll-call votes carry a per-country breakdown), the vote-code legend, how topics are identified, and the known data-quality caveats below.

Exports. Every chart carries a CSV button (the exact data behind it) and a PNG button (the chart itself, rasterised client-side — resolved palette colours inlined, current palette's paper background); the data tables carry CSV. All export is offline/client-side, so it works in the single-file build too. scripts/audit.py sweeps both desktop and mobile viewports for overflow/console bugs; scripts/export_test.py downloads and validates every CSV + PNG.

Topics come straight from the library catalogue — no ML/topic-modelling — so every label is authoritative and traceable. The thematic/country split is a heuristic (build_dashboard_data.py) and editable. Coverage/reconciliation stats shown in the Methodology tab are computed at build time in build_dashboard_data.py (meta.coverage) so they can never drift from the underlying CSVs.

Rebuild the payload after re-harvesting: python scripts/build_dashboard_data.py.

Reproduce

python3 -m venv .venv && source .venv/bin/activate
pip install playwright && playwright install chromium
python scripts/harvest.py        # ~2 min; resumable (skips years already on disk)
python scripts/parse_marcxml.py  # writes data/csv/*.csv

resolutions.csv

column source notes
record_id MARC 001 stable Invenio id; record_url links to the page
symbol 791$a e.g. A/HRC/RES/61/29, E/CN.4/RES/2005/1
title, statement 245$a$b, 245$c
date, year 269$a → ISO falls back to 992$a
body 981$a Commission on Human Rights / Human Rights Council
vote_type 591$a RECORDED · ADOPTED WITHOUT VOTE · NON-RECORDED · WITHDRAWN · NOT CONSIDERED …
meeting, meeting_type 952$a, 793$v
agenda_item_no/title/symbol/subject 991$b/$c/$a/$d
main_sponsors 500$a parsed from the "Main sponsors :" note
yes,no,abstain,nonvoting,total 996$b/$c/$d/$e/$f official vote totals
n_rollcall count of 967 number of per-country rows in votes_long
url_resolution, url_draft 856$u undocs.org links

votes_long.csv

One row per country per resolution. record_id joins to resolutions.csv.

column meaning
iso3, country 967$b (ISO-3166 alpha-3), 967$e (name as catalogued)
vote see legend below

Vote code legend (967$d):

code meaning rows
Y Yes 44,536
N No 18,368
A Abstain 15,911
. Absent / did not participate 701
`` (empty) Not a Council member at the time / no position recorded 643

Only ~1,705 resolutions (RECORDED votes) carry a per-country roll-call; the rest were adopted without a vote or only totals were published (totals are still in resolutions.csv).

Data-quality notes (source, not parsing)

  • ~148 recorded votes (~9%, mostly older CHR) have a roll-call whose Y/N/A counts don't perfectly reconcile with the official 996 totals — almost always a discrepancy in abstentions / non-participation, occasionally a blank vote code where the total implies a position. These are inconsistencies in OHCHR's own MARC cataloguing; the data is preserved as-is (996 = official totals).
  • One typo (yY, UK 2011) is normalised; nothing else is altered.

Deploy (GitHub Pages)

Pages is served from dashboard/ by the workflow in .github/workflows/pages.yml (Pages source = GitHub Actions). Every push to main that touches dashboard/** redeploys. To refresh the data end-to-end:

python scripts/harvest.py            # re-harvest MARCXML (needs Playwright + a browser)
python scripts/parse_marcxml.py      # -> data/csv/*.csv
python scripts/build_dashboard_data.py   # -> dashboard/data.js
python scripts/build_world_map.py    # -> dashboard/world.js  (only if geography changes)
python scripts/smoke_test.py         # file:// regression check, must pass
git commit -am "refresh data" && git push   # Actions redeploys Pages

Citation

If you use this software, the data pipeline, or the derived datasets, please cite it (see CITATION.cff; GitHub's "Cite this repository" button generates APA/BibTeX from it):

Szoszkiewicz, Ł. (2026). OHCHR Voting Records — CHR · HRC (v1.0.0) [Software]. Zenodo. https://doi.org/10.5281/zenodo.21281232

License

Code and data are released under the PolyForm Noncommercial License 1.0.0 (LICENSE) — research, education, non-profit, and personal use are permitted; commercial use requires a separate licence from the author. The underlying voting records are public UN documents from the OHCHR Search Library. Web fonts are loaded from Google Fonts under their own SIL Open Font License.

Harvest gotchas (why the code looks the way it does)

  • The site sits behind AWS WAF with a JS challenge — plain curl/fetch get a 202 with an empty body. A real browser (Playwright Chromium) solves it silently and yields an aws-waf-token cookie (valid a few minutes; the script re-solves on demand).
  • Invenio hard-caps deep pagination at jrec=5000: paging the full result set returns only ~5,199 unique records and then repeats the tail. Per-year partitioning (p=year:YYYY, each < 300 records) stays under the cap and is provably complete. of=id does not paginate; OAI-PMH exposes no Voting set.

About

Interactive dashboard and reproducible dataset of CHR and Human Rights Council voting records from the OHCHR Search Library, 1946–2026

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors