Skip to content

📊 Refresh the long-run history of child mortality static viz - #6701

Draft
paarriagadap wants to merge 8 commits into
masterfrom
data-refresh-childmortality-staticviz
Draft

📊 Refresh the long-run history of child mortality static viz#6701
paarriagadap wants to merge 8 commits into
masterfrom
data-refresh-childmortality-staticviz

Conversation

@paarriagadap

@paarriagadap paarriagadap commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Written by Claude Opus 5 — @paarriagadap at the wheel.

Rebuilds "The long-run history of child mortality" as a reproducible export://static_viz step, and brings the data it needs into ETL. The image it replaces (Youth-mortality-rates-over-last-two-millennia-updated-to-2022.png) is hand-drawn, so none of it could be re-derived, and its modern figures were two data revisions stale.

Figma page: long-run-history-child-mortality20260827 The long-run history of child mortality (Pablo A).

What's here

snapshots/papers/2026-08-18/volk_atkinson_2013.csv Tables 1 and 2, transcribed by OWID
snapshots/papers/2026-08-18/knodel_1970.csv Table 11, transcribed by OWID
data://garden/papers/2026-08-18/child_mortality_in_the_past 5 tables: historical societies, hunter-gatherer societies, the global series, national extremes, and the averages
export://static_viz/papers/2026-08-18/child_mortality_long_run the chart, 850×1095

Garden-only — nothing reaches the grapher DB, so there are no new variable IDs and no charts to remap.

Numbers

All 21 historical values reproduce the published chart exactly (62, 57, 55, 53, 51, 51, 50, 50, 50, 49, 48, 48, 47, 45, 45, 45, 44, 41, 40, 40, 40), and their average comes out at 48.10% → 48%, the figure the chart states. Nineteen come from Volk & Atkinson's Table 2; Sweden 1751–80 is computed from Human Mortality Database life tables (39.5%, matching the published point) and Bavaria 1750–99 from Knodel's counts (50.0%).

Modern anchors are updated: 1950 = 26.3% (the old chart said 27%) and 2024 = 4.32% (it said 2022 = 4.3%). Niger still has the highest national rate, now 13.7% rather than 15%.

Both producers are current — UN IGME's 2025 release (published 2026-03-17) is in ETL, and UN WPP's 2026 revision has been postponed to 2027, so the 2024 revision remains the latest.

Two things about the sources, recorded rather than smoothed over

Volk & Atkinson's published means do not follow from their own tables. Table 1 states a mean child mortality rate of 48.8% across 17 societies; the 17 printed rates average 47.3%. Table 2 states 46.2%, which recomputes to 47.0%. In both tables the infant-mortality N is one lower than the count of printed values. The hunter-gatherer average is therefore carried through as published and never recomputed, while the historical average is our own computation — and the gap between them is asserted, so a future transcription error cannot hide inside it.

Knodel's column counts children who survived to 15, not children who died. For the cohort used here the two coincide (5.6 born, 2.8 surviving, so 2.8 died), which makes the column easy to misread, so the subtraction is written out explicitly. All five of his marriage cohorts are transcribed, and the meadow step reconciles them against the four figures his own Total row prints.

Layout, and the shared module

The step uses etl.static_viz for the Figma handoff contract — apply_svg_rcparams, export_frame (the clip sweep, the opaque-PNG/transparent-SVG split, and the figsize check against the template) and source_citation — rather than restating it.

Two fixes came out of building the Figma page:

  • Multi-line labels are drawn one text call per line, on an explicit baseline. A "\n" handed to a single ax.text gets no text-anchor at all, so those lines lose their alignment the moment Figma re-renders them in Lato. 12 of 33 in-plot labels were affected, including both centred average labels; all 33 now carry one. Invisible in the PNG, which is why it needed a mechanical check.
  • The header rhythm dropped its logo term. The design team flattened the Vertical template's header into [title, subtitle] with the logo as a sibling, so it no longer sets the title row's height and this chart's one-line title stops inheriting 12.5px of dead space. Verified against the live frame with verify_templates.js (no drift on any of the ten templates), and the band now lands at the 89 the frame itself measures.

The 21 labels are placed by a deterministic search against measured text boxes, with a hairline leader wherever a label lands more than 1.4pp from its mark. The license line drops the words "the author", because two names overrun the slot it shares with the tagline.

One departure needing design sign-off

The published chart drew a faded line from Sweden's 18th-century point down to the 1950 global rate. No data supports it — UN IGME and UN WPP begin in 1990 and 1950 — and it reads as one country's history continuing into the world's. It is dropped, and the global series begins where its sources do. The empty band that leaves is real.

Still open

  • Design review, on three things: the dropped bridge line above; whether the value axis should stay unlabelled (every mark carries its own value, as the published chart did); and the title/subtitle shape — the static templates ship a two-line title over a one-line subtitle and this chart is the inversion, which the Figma skill warns reads wrong. Fixing the last one means moving the age cut-off sentence from the subtitle into the Note, which is a user-facing text change and not mine to make.
  • TEMPLATES.md in /create-static-viz still documents the pre-flattening header rhythm, so it now disagrees with NODE-MAP.md on a fact it owns. Its own paired-skill rule says to correct it; worth a separate PR.
  • The article's five build-up strip images show the same chart in pieces and will disagree with the refreshed version. Separate piece of work.
  • A data bug in igme_under_fifteen_mortality, unrelated to this PR but live: Central African Republic's rate spikes to 60.4% (2009), 33.0% (2019) and 54.1% (2022) against ~11% either side. It is in the published youth-mortality-rate data, most likely from the vintage/current combination in garden/un/2026-06-09/igme.py. To raise with that dataset's owners separately.
  • Surrounding prose on the topic page and article quotes figures that have moved (0.4% → 0.3% for the lowest countries) and needs a pass alongside the image swap.
  • Checks not run, named rather than implied: verify_page.js (the diff_against_template.js gate was run instead, and reports the frame matches the template), the Good Data Viz Checklist, and the adversarial-data-review / metadata-style / typo skills. Figma comments cannot be read over MCP, so the design review is outstanding rather than clean.
  • Nobody has checked how the chart reads at the sizes it is actually embedded at, or on mobile — the mobile templates have no room for 21 labels, so whether a mobile version is feasible is a question for design rather than an assumption.

@owidbot

owidbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Quick links (staging server):

Site Dev Site Preview Admin Wizard Docs Docs Preview

Login: ssh owid@staging-site-data-refresh-childmortality

chart-diff: ✅ No charts for review.
data-diff: ❌ 1 new — full report
+ garden/papers/2026-08-18/child_mortality_in_the_past (new dataset: 5 table(s), 24 column(s))

Automatically updated datasets matching excess_mortality|covid|fluid|flunet|country_profile|garden/ihme_gbd/2019/gbd_risk are not included. Run locally with etl diff REMOTE data/ --include <dataset> --verbose.

Edited: 2026-08-28 17:33:56 UTC
Execution time: 14.44 seconds

paarriagadap and others added 7 commits August 18, 2026 15:08
Replaces the hand-drawn Youth-mortality-rates-over-last-two-millennia-updated-to-2022.png
with a reproducible export://static_viz step, and brings the data it needs into ETL.

Data
- New snapshots: Volk & Atkinson (2013) Tables 1 and 2, and Knodel (1970) Table 11, both
  transcribed by OWID and verified cell by cell against the papers' own column geometry.
- New garden dataset papers/2026-08-18/child_mortality_in_the_past: the 21 historical
  societies the chart plots, the 17 hunter-gatherer societies, the global series, the
  national extremes, and the averages the chart states in words.
- All 21 historical values reproduce the published chart exactly, and the average comes out
  at 48.10% -> 48%, the figure it states. Sweden 1751-80 is computed from HMD life tables
  (39.5%) and Bavaria 1750-99 from Knodel's counts (50.0%).
- Modern anchors updated: 1950 = 26.3% (was 27%) and 2024 = 4.32% (was 2022 = 4.3%). The
  global series splices UN WPP life tables before 1990 onto UN IGME from 1990.

Two things about the sources are recorded rather than smoothed over. Volk & Atkinson's
published means do not follow from their own tables - Table 1 states 48.8% where the 17
printed rates average 47.3% - so the hunter-gatherer figure is carried as published and
never recomputed, and the gap between the two is asserted. And Knodel's column counts
children *surviving* to 15, not children dying; for this cohort the two coincide, so the
subtraction is written out to stop the column being read as deaths.

Layout follows the static-chart Vertical template's auto-layout rhythm rather than its
placeholder y values, at the template's own measured type sizes. The license line drops
"the author" because two names overrun the slot it shares with the tagline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…attened header

Master's shared static-viz module and the design team's template edits both landed after this
step was written, so it was restating a contract that now exists as code and laying itself out
against a header structure that no longer exists.

- Adopt `etl.static_viz`: `apply_svg_rcparams`, `export_frame` (which owns the clip sweep, the
  opaque-PNG/transparent-SVG split and the figsize check against the template) and
  `source_citation`. That deletes the step's own citation builder and its preamble.
- Draw every multi-line in-plot label as one text call per line, on an explicit baseline. A "\n"
  handed to a single ax.text gets no text-anchor at all, so those lines lose their alignment when
  Figma re-renders them in Lato. 12 of 33 in-plot labels were affected, including both centred
  average labels; all 33 now carry an anchor.
- Drop the logo from the header rhythm. The design team flattened the Vertical template's header
  into [title, subtitle] with the logo as a sibling, so it no longer sets the title row's height
  and this chart's one-line title stops inheriting 12.5px of dead space. Verified against the live
  frame with create-figma-chart's verify_templates.js, which reports no drift on any template, and
  the band now lands at the 89 the frame itself measures.
- Write the Human Mortality Database out in full in the source line. Keying the citation on
  attribution_short is what collapses UN IGME's two releases into one entry, but it renders HMD as
  an acronym a reader of the chart would not recognise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tself

Following the rule master just added to create-figma-chart: a deep link handed over in chat is gone
by the next session, and "where in Figma is this?" then means searching a ~200-page file by eye.

Records the page, both frames with their node ids and a deep link, and says in the same breath that
the ids are a convenience while the frame name is the durable join - it is the kebab-case slug the
website exports the PNG by, so it is the one string shared by the layer panel, the filename and this
file.

Also writes down how the page was built, to the bar of redoing it from this file alone: the template
node and the verify-before-cloning rule, the upload route and the cropped-raster trap, the 850/816
rescale and where it comes from, the full list of gid suffixes the accent binding has to name (a
missed one leaves a mark on its matplotlib colour, which is how the paired viz shipped its outer band
unbound), the two library style keys, and the weight runs each footer slot has to get back after
`characters` collapses them.

Includes one negative result, so it is not re-derived: Figma fits a whole number of dash repetitions
into each segment of a path, so a dashed line follows its vertex spacing rather than its pattern.
This chart's gridlines are two-vertex spans of 589pt - one segment each, far above the ~50px where
that starts to matter - so they need no resampling. A dashed line following a curve here would.

Outputs are byte-identical; this is a docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…E axis

Two defects inherited from the published chart, both about the calendar rather than the data.

**There is no year 0.** The calendar runs 1 BCE straight into 1 CE, and the chart asserted the
missing year twice: an axis tick reading "0", and Roman Egypt labelled "around the year 0". The tick
is gone - 0 on this scale is the boundary between the eras, not a year to label - and Volk &
Atkinson's printed "0 A.D.", which is not a year either, is read in garden as the turn of the era it
means. The snapshot keeps the paper's own string; garden resolves it and asserts that no row's
period_start or period_end is 0. period_mid still can be, and Rome's is: it is a position on a
continuous scale, not a claimed year, which is why only the endpoints are checked.

**The BCE stretch of the axis had no tick at all.** Three of the 21 societies are dated in BCE and
the leftmost tick was the era boundary, so a third of the axis carried no scale and the labels
mentioned an era the axis never named. Adds a tick at 500 BCE - the round century between the undated
hunter-gatherer marker and the earliest dated society, so it gives that stretch a reading without
landing under the marker and appearing to date it.

Both fixes need the eras naming, because "500" beside "500 BCE" is exactly the pair a reader
misreads. Ticks and period labels now carry CE below the year 1000 and omit it above, so the axis
reads 500 BCE / 500 CE / 1000 / 1500 / 2024 and a period reads "200 BCE-200 CE" or "300-550 CE".

The wider labels re-solved on their own: the label placement search found slots for all 21, and the
trailing axis margin is derived from the widest label rather than fixed.

The Figma frame is updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…in their placement

Each society's label put its name, its rate and its period at one size, so the three competed and the
reader had to parse the run to find the number. The name and the rate now sit at 13px and the period
at 12px, one step apart on the annotation ladder, on the same baseline.

The size the labels ran at before was 8pt - 11.1 template px, under the 12px floor for in-plot text.
The quieter of the two lines now sits on that floor rather than below it.

Stacking the period onto a second line was built first and reverted. It reads the same and costs 2.3x
the height, which this plot does not have: all 21 placed only by drifting up to 19.1 percentage points
from their marks, on fifteen leaders, with four labels stranded in the empty band below the cluster.
Ranking on one line trades that back for width, and width stopped being the binding constraint once
the two bugs below were fixed. Measured over the same 21 labels: worst drift 19.1pp -> 8.4pp, median
7.3pp -> 3.8pp.

Two defects in the placement search, neither of which surfaced as an error - the search just returned
a worse answer:

- **A label was blocked by its own mark.** The gap a label sits at (4px) is smaller than the clearance
  its mark reserves (3px + 1.5px of padding), so every label overlapped its own mark's box and the
  slot level with it - the one the ladder tries first, and the only one needing no leader - was
  rejected for all 21.
- **A stale reserve fenced off the empty band.** The global series' label column was reserved to the
  left of the series, from before those labels were moved to its right. It protected space nothing
  occupies while blocking the band between the pre-modern cluster and the modern line.

The offset ladder is now derived from each label's own height rather than listed, since rungs finer
than the block is tall only offer the search slots that cannot clear. The step logs how far it had to
push, so a run that only just fits is visible rather than silent.

Every one of the 54 in-plot labels carries a text-anchor, checked against the emitted SVG. The Figma
frame is updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants