Live dashboard: https://lszoszk.github.io/hrc-voting/
Human rights voting records of three UN organs — the Commission on Human Rights (CHR, 1946–2006), the Human Rights Council (HRC, 2006–present) and the General Assembly's Third Committee (1970–present) — parsed into dashboard-ready CSVs and an interactive, zero-build static dashboard. The Commission and Council come from a complete harvest of the Voting collection of the OHCHR Search Library (https://searchlibrary.ohchr.org/search?c=Voting); the Assembly from the UN Digital Library and the Third Committee's own records (see below).
Coverage: 7,406 records across three organs, 1946–2026. The CHR/HRC harvest is complete against the OHCHR catalogue as of 26 September 2026 — 6,392 records (resolutions, decisions, presidential statements and 749 amendments to draft texts), of which 1,716 went to a roll-call (80,676 individual country votes); the record count matches the collection total exactly, verified by three independent harvest methods. The catalogue itself omits a few Council texts — two rejected drafts and 21 amendments in 2020–2024, checked against the session reports (see Data-quality notes). The Assembly is roll-call only: 1,014 Third Committee resolutions adopted by recorded vote (130,591 plenary votes, plus 126,080 committee-stage votes), because no equivalent catalogue of its consensus adoptions exists. 2,730 records in total carry a per-country breakdown.
General Assembly extension (September 2026). The dashboard's Bodies selector
adds the Assembly's human rights resolutions — the texts that come out of the
Third Committee — as a third organ next to the Commission and the Council:
708 resolutions adopted by recorded vote (sessions 25–80, 1970–2025; 130,591
per-State plenary votes) from the UN Dag Hammarskjöld Library's General
Assembly voting data (version 5, February 2026), and the Committee-stage
roll-calls read from the Committee's summary records (A/C.3/<session>/SR.n) on
UN Documents — 747 recorded committee votes (drafts, amendments, paragraphs,
motions; 126,080 State votes; sessions 55–79), covering 438 of the 446
draft-attributed resolutions of those sessions and cross-checked against the
Committee's e-voting sheets (10,480 of 10,482 State-votes agree) — so a
resolution's page shows the committee vote next to the plenary one and names
every State that changed between them. The Committee's 306 other recorded votes
(amendments, separate paragraphs, procedural motions, drafts that never reached
a plenary vote) are records of their own under the amendments scope. Methodology 14 documents the sources, the
attribution rule for pre-2000 records and the terms of use.
- Claude skills (
skills/):hrc-votinganswers questions about how States voted (country records, voting partners, resolution tallies, consensus vs recorded votes, amendments, fact-checking against the Council's session reports) from a bundled offline snapshot, andhrc-voting-citewrites citations, methods notes and captions. Both build the dataset's safeguards into their answers (amendments kept apart from resolutions, an abstention is not a vote against, known catalogue gaps flagged). Download the zips and install steps: https://lszoszk.github.io/hrc-voting/skills/. - MCP server (
lszoszk/mcp-hrc-voting): the same data as nine read-only tools and four reference documents for Claude Desktop, Claude Code and other MCP clients, offline and with the same safeguards. Install:claude mcp add --scope user hrc-voting -- npx -y github:lszoszk/mcp-hrc-voting. - Plain pages for crawlers and assistants: one page per State (
/country/<name>/, with a CSV of its votes) and per recorded resolution (/resolution/<symbol>/), generated fromdata/csv/in the Pages workflow, plus/llms.txt. - Machine-readable data: Hugging Face (
lszoszk/hrc-voting, 10 parquet configs), Zenodo DOI,CITATION.cff, schema.orgDatasetmarkup on the dashboard.
# voting record (1946-2026)
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)
scripts/build_world_map.py # Natural Earth 110m -> dashboard/world.js
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
# General Assembly — Third Committee (sept 2026)
scripts/ga/parse_undl.py # UNDL GA voting CSV -> data/csv/ga_resolutions.csv + ga_votes_long.csv
scripts/ga/harvest_sr.py # Third Committee summary records A/C.3/<s>/SR.n -> data/raw/ga/sr/ (git-ignored)
scripts/ga/parse_sr_votes.py # summary records -> data/csv/ga_committee_events.csv + ga_committee_votes.csv
scripts/ga/harvest_sheets.py # Third Committee e-voting sheet PDFs (un.org, sessions 65-78) -> data/raw/ga/sheets/
scripts/ga/parse_sheets.py # sheets -> data/csv/ga_sheet_events.csv + ga_sheet_votes.csv (cross-check of the SR parser)
scripts/ga/harvest_ga_texts.py # A/RES PDFs -> data/ods_texts/ (same log as the HRC texts; indexed from 1993, earlier PDFs are scans)
data/raw/ga/ # the UNDL CSV (364 MB), its metadata, igov proposal JSON (git-ignored)
# resolution texts (1993-2026) — powers the Texts and Language tabs
scripts/mirror_ap_ohchr.py # mirror CHR 1993-2005 .doc + HRC s1-11 .pdf
scripts/harvest_ods_texts.py # HRC s12+ .pdf from documents.un.org
scripts/build_text_index.py # extract -> clause-segment -> dashboard/texts/ (FTS index)
scripts/tag_terms.py # operative-verb coding -> dashboard/texts/lang.json
data/ap_mirror/, data/ods_texts/, data/text_cache/ # sources + extraction cache
dashboard/texts/ # catalog.json, docs-<year>.json, idx/, lang.json
# Hugging Face package (rebuilt from the committed outputs above)
scripts/prepare_hf_dataset.py # -> huggingface/hrc-voting/data/*.parquet + dashboard/hf_stats.js
huggingface/hrc-voting/ # dataset card + 10 parquet configs (5 CHR/HRC + 5 GA), ready to upload
# Claude skills (see skills/README.md)
skills/hrc-voting/ # query + analyse, with a gzipped snapshot of the CSVs and a stdlib-only CLI
skills/hrc-voting-cite/ # citations, methods notes, licence wording
scripts/build_skill_snapshot.py# refresh the snapshot bundled in the skill (after a re-harvest)
scripts/package_skills.py # zip each skill for upload (the Pages workflow publishes them)
# crawlable pages (generated in the Pages workflow, nothing committed)
scripts/build_static_pages.py # one page per State and recorded resolution, skills page, llms.txt, sitemap
scripts/check_static_pages.py # fails the deploy on a broken link, duplicate title or collapsed page count
# app + checks
dashboard/index.html # self-contained static dashboard (open directly)
dashboard/data.js # embedded data payload (~2.4 MB)
scripts/smoke_test.py # file:// regression check, must pass before shipping
scripts/audit.py # desktop + mobile overflow / console sweep
scripts/export_test.py # downloads and validates every CSV + PNG
scripts/check_figures.py # fails if a figure stated in prose drifted from the build
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 ~2.7 MB file with data.js and world.js inlined; regenerate it with
python scripts/build_single_file.py after any rebuild (the Texts and Language
tabs fetch shards over HTTP and stay inert there by design, with a notice).
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. Seven tabs plus a Methodology & sources page reached from the footer: four over the 2,730 recorded (roll-call) votes, an experimental Consensus view over the 6,392 catalogued CHR/HRC records (the Assembly has no consensus catalogue, so that view is scoped out under Assembly-only), and two experimental views over the 5,113 harvested resolution texts (1993–2026):
- Overview — volume over time (CHR→HRC), how contested votes were, most divided votes.
- Countries (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.jsgenerated byscripts/build_world_map.pyfrom 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 (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 on the Methodology & sources page (footer link).
- Consensus (experimental) — reads the mode of adoption as a diplomatic signal
across ALL 6,392 catalogued CHR/HRC records (
resAllin the payload, trimmed titles): breakdowns & rapprochements (subjects whose mode flipped consensus↔votes — the hero panel, e.g. HIV/AIDS 94%→0%), consensus share per year (peak 86%, HRC low 69% in 2021; amendments excluded, as in the default Scope), a 100% adoption-mode stack, most/least consensual subjects, a withdrawn- drafts panel (a post-2010 phenomenon), and a filterable explorer of the full catalogue. Modes, thresholds and caveats (without-a-vote ≠ unanimity; small-n flips are candidates, not findings) documented in Methodology. - Texts (experimental) — clause-level full-text search over the 5,113 harvested
resolution texts (1993–2026), the earliest the OHCHR/ODS document mirrors reach.
Results are shown clause by clause with preambular / operative labels; AND / OR /
"exact phrase"/prefix*/-exclude, plural folding, relevance or date sort. Index built bybuild_text_index.pyinto per-year bundles + per-initial posting shards, intersected client-side. Needs HTTP (it fetches shards), so it is inert in the single-file offline build. - Language (experimental) — operative-verb commitment analysis over the same
1993– corpus. Each operative paragraph is scored by its leading verb on two axes:
directive force (D5 Decides … D2 Invites) and sentiment (−2 Condemns …
+2 Commends), plus a delegation flag for clauses that create or task machinery.
Context-scored verbs (Expresses its grave concern vs its appreciation) resolve
per clause and ship their split — grounded in a 2001 UN Office of Legal Affairs
opinion on "takes note of" / "notes" (GA decision 55/488). Amendments and the
articles of annexed declarations and protocols are excluded — they are edit
instructions and treaty-style text, not commitments of the organ. Vocabulary is
cross-checked against UNITAR NYO, Guidelines for United Nations Resolutions
(2020),
Annex VI–VII, in addition to the JG-1/02/10 glossary; caveats in
tag_terms.pyand Methodology. - Methodology & sources (footer link) — explains, with live numbers pulled from
meta.coverage, why the vote-based views operate on 2,730 records rather than the whole catalogue (only recorded/roll-call votes carry a per-country breakdown), the vote-code legend, how topics are identified, every alignment/consensus/language measure defined, 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 & sources page 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.
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| 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 |
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,716 of the CHR/HRC 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). The Assembly's votes live in ga_votes_long.csv and
ga_committee_votes.csv.
- 49 recorded votes (~3%; 31 Council, 18 Commission) 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). The July 2026 harvest had 148 such votes; OHCHR has since corrected most of them.
- Three 967$b codes contradict the country name in 967$e and are repaired by
parse_marcxml.py(repair_iso), which trusts the name and takes the code from the corpus majority: rec 21027 HUNGARY wasHND, rec 21028 NORWAY wasPER, rec 30508 BRAZIL wasBEN. Left alone each moved one vote onto another state's profile. Every repair is printed at parse time. - One vote-code typo (
y→Y, UK 2011) is normalised; nothing else is altered. - 12 calendar years (1949, 1951, 1953, 1956–57, 1959–63, 1965, 1973) contain no recorded vote at all. A state casting no roll-call in one of those years says nothing about its membership, so the Country profile draws no marker there.
- One roll-call row (rec 24737,
E/CN.4/RES/6(XX)) has a malformed 967 with no country and is dropped from the dashboard payload — hence 80,675 country-votes in the app against 80,676 rows invotes_long.csv. - The OHCHR catalogue omits some Council texts. Checked against the session
reports (
A/HRC/<session>/2) for sessions 43–59 (2020–2025), symbols and outcomes agree except that the catalogue lacks two of the Council's three rejected drafts (A/HRC/48/L.11, Yemen; A/HRC/51/L.6, debate on Xinjiang) and 21 amendments (A/HRC/48/L.49–L.57; A/HRC/49/L.43; A/HRC/56/L.27, L.28, L.37, L.38, L.43, L.50, L.52–L.56). The votes on 26/L.37 and 29/L.37 are no-action motions (Yes = take no action). All are kept as published; Methodology §06 lists them with the report references. New sessions reach the catalogue some weeks after they close.
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 (prints any iso3 repairs)
python scripts/build_dashboard_data.py # -> dashboard/data.js
python scripts/build_world_map.py # -> dashboard/world.js (only if geography changes)
# texts (only when new resolution documents are mirrored)
python scripts/mirror_ap_ohchr.py # -> data/ap_mirror/
python scripts/harvest_ods_texts.py # -> data/ods_texts/
python scripts/build_text_index.py # -> dashboard/texts/{catalog,docs-*,idx}
python scripts/tag_terms.py # -> dashboard/texts/{lang,terms}.json
# must precede build_single_file.py: it writes dashboard/hf_stats.js, which the
# single-file build inlines and Methodology 13 renders its config table from
python scripts/prepare_hf_dataset.py # -> huggingface/ + dashboard/hf_stats.js
python scripts/build_single_file.py # -> dashboard/OHCHR_voting_dashboard.html
python scripts/check_figures.py # prose figures / version / title vs the build
python scripts/smoke_test.py # file:// regression check, must pass
git commit -am "refresh data" && git push # Actions redeploys Pageshuggingface/hrc-voting/ holds a ready-to-upload package as parquet, plus a dataset
card. Live at lszoszk/hrc-voting.
Rebuild it from the committed CSVs and text index with:
python scripts/prepare_hf_dataset.py
hf upload lszoszk/hrc-voting ./huggingface/hrc-voting --repo-type=datasetTen configs: five over the CHR/HRC record (resolutions, votes, clauses,
countries, subjects) and five over the General Assembly Third Committee
(ga_resolutions, ga_votes, ga_committee_events, ga_committee_votes,
ga_clauses). Exact row counts are in dataset_stats.json, written by the same run.
The card leads with the analytical traps in this data (roll-call selection, abstentions
not being votes against, amendments not being resolutions, the ~3% reconciliation gap)
and the package ships derived columns — prevailing_side, adopted,
rollcall_reconciles, clause_type — so downstream users do not have to rediscover
them. The PolyForm Noncommercial licence carries over, so the card declares
license: other.
The same run writes dashboard/hf_stats.js, which Methodology §13 renders its config
table from. Nothing about the package is restated by hand in the dashboard: adding a
config without a description in HF_DESC fails the build rather than shipping a table
that cannot describe it.
The README's own figures cannot be derived that way — markdown is read as written — so
they get the other half of the pattern. python scripts/check_figures.py compares every
coverage figure, the config count, the version and the title against
dataset_stats.json, data.js and CITATION.cff, and exits non-zero naming whichever
drifted. Run it after a rebuild, before pushing.
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). UN Human Rights Voting Records — CHR · HRC · GA Third Committee (v1.2.0) [Software]. Zenodo. https://doi.org/10.5281/zenodo.21281232
The dashboard surfaces the same line in two places: a Cite button in the footer
(copies it to the clipboard from any tab) and Methodology §12, which also explains
the concept-vs-version DOI distinction and how to cite the underlying UN votes rather
than this software. Both read from a single constant (CITE_ACADEMIC in
dashboard/index.html) — keep it in sync with CITATION.cff and .zenodo.json
whenever the version, release date or DOI changes.
Built and maintained by Łukasz Szoszkiewicz (Adam Mickiewicz University, Poznań) · ORCID 0000-0001-6671-2893.
The first version of this tool was inspired by Alfonso Garcia Miguel.
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.
- 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 anaws-waf-tokencookie (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=iddoes not paginate; OAI-PMH exposes no Voting set.