Excel and CSV Data Analysis
bytedance/deer-flow
Analyzes uploaded Excel and CSV files with SQL through DuckDB, producing schema inspections, statistical summaries and exports to CSV, JSON or Markdown.
Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.
$ npx skills add owid/etl --skill map-charts-to-mdim -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install owid/etl map-charts-to-mdim --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .claude/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .claude/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdimType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add owid/etl --skill map-charts-to-mdim -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install owid/etl map-charts-to-mdim --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .agents/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .agents/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add owid/etl --skill map-charts-to-mdim -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install owid/etl map-charts-to-mdim --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .cursor/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .cursor/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/owid/etl.git --path .claude/skills/map-charts-to-mdim--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add owid/etl --skill map-charts-to-mdim -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install owid/etl map-charts-to-mdim --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .gemini/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .gemini/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install owid/etl map-charts-to-mdimInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add owid/etl --skill map-charts-to-mdim -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .github/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .github/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add owid/etl --skill map-charts-to-mdim -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install owid/etl map-charts-to-mdim --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/map-charts-to-mdim .opencode/skills/map-charts-to-mdim && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "map-charts-to-mdim" agent skill from https://github.com/owid/etl/tree/master/.claude/skills/map-charts-to-mdim into .opencode/skills/map-charts-to-mdim/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "map-charts-to-mdim", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
map-charts-to-mdimPropose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.
Map Charts To Mdim is an agent skill from owid/etl. Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs. Selects charts by tag, slug list, or dataset id; matches each chart to MDIM views by indicator IDs (auto-discovering ALL published MDIMs as targets by default); writes a proposal CSV, one redirect payload JSON per source chart, a side-by-side review HTML, and the ;-delimited CSV that grapher's createMultiDimRedirectsFromCsv CLI consumes. Also audits every article, explorer, narrative chart, data insight, static viz…
Its SKILL.md is about 9.2k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including scripts (for example `scripts/audit_references.py`, `scripts/build_review.py` and `scripts/extract_and_match.py`).
It sits in Data & Analytics, covering CSV and tabular files. The repository describes itself as: A compute graph for loading and transforming OWID's data. The licence is MIT.
6 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 70c9705. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 4 files in scripts/ (Python), which the agent can run.
Shell commands in SKILL.md call:
pythonFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Map Charts To Mdim loads about 9.2k tokens when it runs. Until then it costs about 255 tokens; SKILL.md has 5,020 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
name — check what exists (`ls .env*`); on some machines it's `.env.prod`, onothers `.env.live`.ls .env* 2>/dev/nullAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from owid/etl at commit 70c9705, republished under its MIT licence (© owid). 5,020 words, ~9,200 tokens.
.claude/skills/map-charts-to-mdim/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.When individual grapher charts are being retired in favor of MDIMs, each chart
needs a redirect to the equivalent MDIM view. Unlike explorers (see
map-explorer-to-mdim), no human mapping rules are needed: a chart is a single
view, and the match is computed from indicator IDs.
This skill proposes and validates; it never writes. Redirects are created by
a human running grapher's createMultiDimRedirectsFromCsv CLI, which also
unpublishes the source charts in the same transaction. Everything here builds
toward that one command and proves in advance that it will succeed.
The target mechanism is the multi_dim_redirects table (source path →
multiDimId + viewConfigId). Grapher's admin API can write it too, but not
for these charts: it rejects any source that already has an old slug pointing
at it, and its bulk endpoint accepts explorer sources only. The CLI
(owid-grapher/devTools/createMultiDimRedirectsFromCsv.ts) exists precisely for
the chart case.
And only for that case: one bare chart path per row. Two shapes the CSV pipeline cannot express. Neither is worked around by editing the CSV — one has another route, the other needs the CLI changed:
?
survives into multi_dim_redirects.source verbatim, and chart sources are baked
as a flat slug → path map that strips only the /grapher/ prefix
(getGrapherToMultiDimRedirects → getMultiDimRedirectTargets), so the row
matches no request and the redirect never fires. Conditioning on the incoming
params is what the sourceQueryParams column does, and the CLI never writes it —
grapher honors it for /explorers/ sources only (they bake to a query-param
decision tree resolved at the edge), and the admin route rejects it outright on a
/grapher/ source. A per-params chart redirect therefore needs changes on both
sides, CLI and serving. preflight.py rejects a source carrying a query string
for this reason./explorers/... passes the CLI's source regex, and the row
it writes is worse than inert: with sourceQueryParams NULL it bakes as an
unconditional rule, and explorer redirects are consulted on every request
(unlike chart redirects, which fire only on a 404), so the entire explorer —
every view, whatever the params — starts 302-ing to the single target view. The
chart handling it also skips, silently: the source is never unpublished
(getChartSlugsToUnpublish skips non-/grapher/ sources), and the chained
old-slug migration early-returns. Explorers go through the admin bulk endpoint,
which is map-explorer-to-mdim's job, not this skill's.Exactly one chart selection:
--tag "<tags.name>" — published charts with that exact tag (the script
prints nearby tag names, e.g. Economic Inequality vs Economic Inequality by Gender, so the user can widen the selection knowingly).--slugs a,b,c or --slugs @file — explicit chart slugs.--dataset-id N — published charts using any variable of that dataset.Targets: all published MDIMs by default (auto-discovery); restrict with
repeatable --mdim <catalogPath> (as in multi_dim_data_pages.catalogPath).
Charts and MDIMs are read from the grapher DB via OWID_ENV — read-only is
enough for the proposal phase. Point OWID_ENV at a DB that has both:
STAGING=1 (the current branch's
staging-site-<branch> DB; STAGING=<name> for another branch's). Being
checked out on the branch is NOT enough by itself — without the prefix,
OWID_ENV silently points at your local dev DB, which also passes the
connection preflight.ENV_FILE=<prod creds file> DATA_API_ENV=production. Don't assume the file
name — check what exists (ls .env*); on some machines it's .env.prod, on
others .env.live.ENV_FILE=<their file>.If no suitable env file exists and there's no staging server to point at, stop and ask the user — never hardcode credentials.
The extractor prints the environment it resolved (grapher DB: ...) — check
that line matches what you intended before trusting the output. All links in
the artifacts (and the review HTML panes) default to that environment's site,
so a staging extraction is reviewed against staging; --host overrides.
ls .env* 2>/dev/null
ENV_FILE=<file> DATA_API_ENV=production .venv/bin/python -c \
"from etl.config import OWID_ENV; print('DB OK:', OWID_ENV.read_sql('SELECT 1 AS x').iloc[0,0])"ENV_FILE=<prod creds> DATA_API_ENV=production .venv/bin/python \
.claude/skills/map-charts-to-mdim/scripts/extract_and_match.py \
--tag "Economic Inequality" \
--out ai/economic-inequality-charts-mdim-mappingWrites into --out:
| file | contents |
|---|---|
charts.csv | selected charts + their indicator slots |
multidim_views.csv | every candidate MDIM view (row ids A1…, B1…) |
mapping_proposal.csv | one row per chart: quality, target view, clickable URLs |
mapping.json | combined machine record — confident, conflict-free matches only |
payloads/<chart_slug>.json | one JSON per source chart — the copy-paste handoff unit |
redirects_for_cli.csv | the apply input — ;-delimited source;target for the grapher CLI |
migration_log_template.csv | (old_slug, mdim_slug, view_id, cutover_date) — stamp the date at apply time |
unmatched.md | everything not proposed, with candidates/near-miss detail |
mdim_suggestions.md | what the MDIMs would need for the unmatched charts to become redirectable |
HANDOFF.md | standalone note for whoever runs the CLI — command, caveats, why the CLI |
_sources.json | machine record of run inputs (don't hand-edit) |
Handoff convention: one source per JSON. Like the explorer→MDIM redirect
deliverables (ai/explorer-mdim-redirects/, one admin_bulk_payload.json per
explorer), each JSON handed over must describe exactly ONE source page. For
charts that means one file per chart, in payloads/. The combined
mapping.json is the machine record that preflight.py and
audit_references.py read — don't hand that one over as a copy-paste payload.
Matching: a chart matches a view when the y-variable-ID sets are equal AND
x/size/color agree (absent == absent) — after decoration indicators are
stripped from x/size/color on both sides: population as Marimekko width / bubble
size, and regions#owid_region as continent coloring (matched by catalogPath, so
version bumps don't matter). Charts and views carry these inconsistently without
changing what data is plotted — an MDIM view adds x=population for its Marimekko
tab, an editor adds continent colors to a line chart — and requiring them to agree
literally drops same-y pairs into none with no report at all (equal y sets means
they aren't even a near miss). A match made across such a difference says so in
the proposal's note column. Content indicators (a scatter's GDP-per-capita x, a
"political regime" coloring) are never stripped. Two guardrails keep the rule from
overreaching: a scatter's x slot is never stripped — on a ScatterPlot,
x=population is the plotted relationship, not decoration (size/color there still
are) — and the population pattern is end-anchored to the raw head-count columns
(#population, #population_historical, #population_projection), because the
same dataset also carries population density columns that are content. Several matching views → tiebreak
on chart type; still ambiguous → reported, never guessed. Any partial indicator overlap
that is not an exact match → near_miss, reported only — not just subset/superset:
a chart plotting {A, B} against a view plotting {A, C} shares A, so calling
it none would assert an overlap check that came out the other way. Quality labels:
exact — one view matches (possibly via the chart_type tiebreak — check the
tiebreak column).forced / skipped — set by overrides.csv (below).ambiguous — several views match; needs a human pick.near_miss — overlapping but unequal indicator sets; the diff is spelled out.none — no view shares the chart's indicators. An accepted outcome — only
matched charts get redirects; don't force the rest.Twin suspects. ID matching is blind to one real equivalence: a dataset that
publishes the same series in two tables (WID/LIS do — inequality#share_top_10__…
for the standalone charts vs incomes#share__…quantile_10… for the MDIM, with
identical values). The extractor flags these — unmatched charts whose y comes from
the same dataset as a slot-compatible view with a similar column name — in the run
report and in mdim_suggestions.md as twin suspects, with the exact
overrides.csv line to use. A suspect is NOT a match: verify first that the two
indicators' values are identical (fetch both
api.ourworldindata.org/v1/indicators/<id>.data.json and compare every
entity-year), then force it, citing the verification in the override note.
Either side carrying several indicators in one x/size/color slot has no
chart-shaped signature, so it is excluded from matching rather than truncated
to the first: a truncated signature can spuriously exact-match a counterpart that
lacks the rest. Charts land in none with the reason in note; views are dropped
from the target pool with a warning. overrides.csv can still force either.
Conflicts vs CLI-required. Matched charts are checked (read-only SQL) against the same conditions the apply CLI validates. Two outcomes, and the distinction matters:
cli_required — the chart has old slugs redirecting into it, or site
redirects pointing at it. Not a blocker: the CLI migrates both. (The admin
API rejects exactly these as redirect chains, which is why it isn't the apply
path.) The old slugs are listed in the proposal and carried into
payloads/*.json as oldSlugs.conflict — a genuine blocker that would abort the CLI's transaction: the
source is already a redirect source, the chart's own slug is itself an old
slug, the target MDIM's /grapher/<slug> is a redirect source, a self-redirect,
or one of the old slugs is already a redirect source elsewhere. These land in
mapping.json → conflicts[] and are kept out of the CSV — and, because being
absent from the CSV is not the same as being finished, they are also listed in
HANDOFF.md with their blocker, next to the already-redirected rows.Charts already redirected to the same target are counted as already_done — no
redirect to create, but they are still published (the extractor only selects
published charts), so they shadow their redirect and it never fires. The CLI
cannot finish these: their source is already a multi_dim_redirects source,
which its own validation rejects, so including them would abort the whole run.
Preflight reports them as MANUAL; unpublish each one in the grapher admin. The
exception is a chart carrying old slugs — hand-unpublishing deletes its
chart_slug_redirects rows and those slugs become hard 404s, so take those to
the Grapher team instead. That manual unpublish breaks embeds exactly as the
CLI's would, so already_done charts sit behind the same reference gate and
appear in the step-4 audit alongside the proposed redirects.
Many charts → one view is legitimate (e.g. a line chart and its map twin) —
surfaced via shared_target_chart_ids, never collapsed.
Side-by-side HTML (chart left, proposed MDIM view right; approve/flag with notes, decisions persist in the browser):
.venv/bin/python .claude/skills/map-charts-to-mdim/scripts/build_review.py \
--mapping-dir ai/economic-inequality-charts-mdim-mappingRows without a target show their candidates / near-miss diff instead of an
iframe, so ambiguous cases can be triaged into overrides.csv from the same UI.
The proposal CSV also works standalone in a spreadsheet (URLs are clickable).
When the review is done, export the decisions (⬇ JSON or ⬇ CSV) — the apply step consumes that file so flagged charts are excluded.
For ambiguous rows (or to force/suppress a match), write
<out>/overrides.csv — it is never overwritten by re-runs:
chart_id,action,note
1234,SKIP,keep this chart live
5678,poverty_inequality/latest/gini#gini|metric=gini__welfare=disposable,picked over the WID twin
9012,01981234-abcd-7def-8123-456789abcdef,forced by view_config_idThen re-run step 1 (same command) — overridden rows become forced/skipped
and mapping.json regenerates.
A redirect only rescues plain hyperlinks. Anything that embeds the chart — explorers, data insights, static viz, article chart blocks — resolves it by id or slug and keeps rendering the old config, so it breaks when the source chart is unpublished (which the apply step always does).
ENV_FILE=<prod creds> DATA_API_ENV=production .venv/bin/python \
.claude/skills/map-charts-to-mdim/scripts/audit_references.py \
--mapping ai/economic-inequality-charts-mdim-mappingThe sweep itself is the shared find-chart-references skill; this script is the
redirect-specific consumer that adds the replacement URLs. It writes
references.csv + references.md, one row per reference. The report is
organized by what the reader does, not by severity tier: embedded charts and
text links sit adjacent in one "Google Doc edits" section (one editing pass
per doc covers both — 🔴 embeds break on unpublish and gate the CLI, 🟡 links
stay functional behind the 301), with reader-facing section names ("Embedded
charts", "Text links", "Front-matter chart URLs") rather than raw ArchieML
tokens (those live in the CSV's component column). Topic-page All charts
entries collapse to a per-page summary and need NO action: the block lists
only published charts (GdocPost.loadRelatedCharts filters on isPublished),
so entries drop out on their own at the next bake — and
no replacement is possible either, because the block is built from charts ×
chart_tags only and cannot list MDIMs; featuring the MDIM on a topic page is
a separate gdoc-authoring change. Featured metrics get their own ⭐ section,
and they are the opposite case: the All charts block heals itself, a featured
metric does not. It is a topic-page slot held by URL, matched on exact pathname
and params only when Algolia indexes, against published records — so
unpublishing empties it silently. It does not block the CLI, for the
same reason a key chart doesn't, but it is unrecoverable afterwards — hence step
4b rather than post-migration cleanup. Narrative charts get their own table
(before the All charts summary), one row per chart: the admin editor link and
parent chart; "create from this view" — the target view carrying that
narrative chart's stored controls, which is both the rendering to match and the
place to create from; "text to re-apply" (see below); the pages that
actually embed it, resolved by a second hop the sweep doesn't make
(posts_gdocs_links on linkType='narrative-chart', both narrative-chart and
key-insights components), each with its doc link and the name to search for;
and the ordered steps. Each row carries only the order that applies to it, on
the rule above: a published page among the users makes create → repoint →
delete mandatory; with none, the row emits the delete → create shortcut that
reuses the same name (a draft reference then keeps resolving, since pages
reference the name). ℹ️ unpublished/draft pages close the report.
Create from the view, not from a create link. Tell the operator to open the
target view and use the chart's own "Create narrative chart" admin control:
the MDIM page builds that control's target from whichever view is on screen
(site/multiDim/MultiDim.tsx), so the new chart is parented to the right view
and inherits the controls set on it. A bare
/admin/narrative-charts/create?type=multiDim&chartConfigId=<viewConfigId> link
looks equivalent but opens a copy of the MDIM's default view, so never hand
that out as the create step.
The new chart opens at the parent view's defaults — the state does not carry over. The control gets the parent right, but not the state on top of it: the dimension selection, the entity selection and tab/time all come up at defaults, and authored text never transfers at all. So the report's "Set by hand after creating" column lists all three groups per chart, in the order they get applied in the editor:
narrative_charts.patchConfigId → chart_configs.config) (the
delta its author typed on top of the parent) plus its stored
queryParamsForParentChart, and listed chart type first (it decides
which other controls exist), then the entity selection (the most visible
thing to get wrong), then the rest alphabetically. Entities are always shown
by name, never as codes: a URL param spells them ZWE~MDG, so those are
resolved against entities before display (unknown codes pass through). The
patch and the params encode the same state, so a URL param is dropped when
the patch already carries the equivalent config key (country ↔
selectedEntityNames, focus ↔ focusedSeriesNames, time ↔
minTime/maxTime) — otherwise the cell asks for one setting twice in two
spellings, and the config form is what the editor's fields expose;title, subtitle, note, sourceDesc,
hideAnnotationFieldsInTitle from the patch. Miss these and the replacement
silently renders the view's wording instead of the text the article was
written around.Every group is diffed against the target view's config, so only genuine
differences are asked for. dimensions and $schema are excluded from the patch
on purpose: the new parent view supplies them, and re-applying the old ones would
repoint the chart at the retired chart's indicators.
Suggest the replacement's name, and check it is free. When a published page
holds the original name the replacement needs a different one — and that name is
permanent, since create rejects an existing name and there is no rename, so
it is not a staging name to tidy up after the delete. The report suggests
<original>-mdim (falling back to -mdim-2, -mdim-3, …), validated against
every name in narrative_charts so the suggestion cannot be the one thing
create refuses; suggestions handed out within a run are reserved as they go. In
the delete-first case there is nothing to suggest — the row names the original,
which the delete frees for reuse.
Admin routes need the admin origin. A narrative chart's where_path is
/admin/narrative-charts/<id>/edit, and the public site does not serve
/admin — prefixing it with the site host yields a link that 404s. Both the
sweep and this consumer route such paths through a helper
(admin_url / absolute_url) that strips the admin root's own /admin suffix
before joining, or the result carries /admin/admin/.
Pure SQL, so read-only credentials are enough. It sweeps both the current slug and every old slug that reaches the chart — references written before a rename point at the old one.
Watch for the param-collision warning. Grapher merges the incoming URL's
params over the redirect target's, so a link carrying ?metric=… overrides an
MDIM dimension of the same name and lands the reader on the wrong view. The
audit flags those explicitly.
Mention this step every time (like /update-dataset step 7); running it is the
user's call, since a wide sweep costs tokens — but preflight gates on the same
embedded references, so it becomes mandatory before applying.
Narrative charts do not block a migration. One parented to a chart owns a
materialized full config and renders from it, so unpublishing the parent leaves it
intact; only its generated "Explore the data" href uses the parent slug, and the
301 covers that. They are classified link, reported by preflight but never gated
on — gating would strand every such chart behind raw SQL for no reader-visible
gain. Do check the href's query params for collisions with the target view's
dimensions (step 4 flags them).
To actually fix one, replace it — don't try to repoint it. The parent columns are INSERT-only, so there is no repointing API and never will be one by design; the route the Grapher devs endorse is to create a new narrative chart from the equivalent MDIM view and delete the old one. Three API facts set the order, and getting it wrong strands you:
So the obvious sequence (delete, then recreate under the same name) is blocked in precisely the case that matters — a narrative chart embedded in a published article. Two paths, and the references audit (step 4) tells you which applies:
No published post references it → delete, then create the replacement with the same name. The name is preserved and nothing else changes.
A published post references it (the usual case) → do it in this order:
Never delete first. The delete will fail, and unpublishing the article to force it through breaks the page for readers.
Do the create from the target view in the site UI, using the chart's own "Create narrative chart" admin control (see the rule above). The three routes do NOT behave alike, so don't describe them interchangeably:
| route | parent view | state to redo by hand |
|---|---|---|
| the view's "Create narrative chart" control | the view on screen — correct | the entity selection and other controls open at that view's defaults, and authored FAUST never transfers |
a bare {admin_site}/narrative-charts/create?type=multiDim&chartConfigId=… link | the MDIM's default view — wrong | everything, on top of a parent you then cannot change |
scripted POST {admin_api}/narrative-charts | whatever parentChartConfigId you send | nothing you include in the config you post |
So: use the control, then set what the report's Set by hand after creating
column lists. Prefer the UI over the API
anyway — AdminAPI has get_narrative_chart and update_narrative_chart but
no create or delete, so scripting means hand-rolled HTTP for both ends of the
swap.
If you do script it, the endpoints are POST {admin_api}/narrative-charts and
DELETE {admin_api}/narrative-charts/<id>:
{
"type": "multiDim",
"name": "<kebab-case-name>",
"parentChartConfigId": "<the MDIM view's chart_configs.id>",
"config": { /* the OLD narrative chart's rendered full config */ }
}You already have parentChartConfigId: it is target.viewConfigId in the
redirect payload for the chart this narrative chart hangs off
(payloads/<chart_slug>.json, or target_view_config_id in
mapping_proposal.csv). That is the MDIM view's chart_configs.id — the same
value find-chart-references reports as config_id on an mdim view row. So
"which MDIM view do I parent the replacement to" is answered by the proposal: it is
the view the chart was going to redirect to. Get the
config from AdminAPI.get_narrative_chart(<old id>)["configFull"]: the endpoint
derives the patch itself by diffing what you pass against the new parent, so pass
the rendered full config, not the old patch.
Replacement is manual, and that is a deliberate call, because the number of narrative charts that can ever be in this situation is tiny. The population is not "all narrative charts" — it is narrative charts whose parent chart is itself a redirect candidate, i.e. has an exact MDIM-view match. Applying this skill's own matching rule to every published chart with narrative children gives a site-wide ceiling of a handful of narrative charts, if every matchable chart were migrated — not a per-migration figure.
Re-measure rather than assuming that ceiling: it grows as MDIMs are published, since
each new MDIM view can turn an existing chart into a candidate. ai/ in this repo
has the query; the shape is — narrative charts on published parents → those parents'
indicator slots → compare against every published view's slot signature. Revisit
the decision if a single migration would require replacing more than a handful,
and say so in the PR rather than grinding through them silently.
A narrative chart parented to an MDIM view blocks that MDIM's next re-publish,
and no issue tracks it. Manual replacement does nothing for this. It happens
because cleanUpOrphanedChartConfigs deletes
multi_dim_x_chart_configs rows with no narrative-chart guard and the FK refuses
(ER_ROW_IS_REFERENCED_2). Renaming a dimension or choice slug is enough to
trigger it, and it surfaces as a data update failing with an opaque FK error.
Replacement makes this more likely, not less: every replacement creates a new
MDIM-parented narrative chart. If the count of MDIM-parented narrative charts on
production grows, file it as its own issue — the write-up is in
ai/narrative-charts-grapher-issue.md (§2), alongside
ai/narrative-charts-slack-post.md.
Work the ⭐ section of references.md, by hand at /admin/featured-metrics. These are editorial
slots, so ask whoever owns the topic before repointing their rail.
Per row — add the MDIM view under the same tag and income group, drag it to the old ranking,
delete the old row, then re-apply boost in search if it was on. One chart can hold several
rows: the key is (URL, tag, income group). Full procedure:
docs/guides/data-work/redirect-to-mdims.md.
Say this out loud: the CLI unpublishes the sources in the same transaction that creates the redirects, and adding a featured metric requires a published slug — so afterwards the old URL is refused and the ranking is gone. This is the one item in the migration that is unrecoverable rather than merely broken. And the replacement is the bare view, not the redirect target: the admin strips reader params on paste and never validates the dimension params, so a redirect target is accepted and then fails silently.
This skill never creates redirects. Applying is yarn createMultiDimRedirectsFromCsv in owid-grapher, run by a human.
First, preflight (read-only, safe to run any time):
ENV_FILE=<prod creds> DATA_API_ENV=production .venv/bin/python \
.claude/skills/map-charts-to-mdim/scripts/preflight.py \
--mapping ai/economic-inequality-charts-mdim-mapping \
--decisions ai/economic_inequality_chart_mdim_review.jsonIt re-runs every validation the CLI performs, against the live DB, and re-checks
both sides of each row against the proposal: the source chart's slug + config MD5,
and the target MDIM's slug + reviewed view (an edited, deleted, renamed or rebuilt
one comes back STALE). Statuses: OK / BLOCKER / EXISTS / DIFFERS /
GONE / STALE / MANUAL.
It also gates on embedded references (step 4) — the one failure mode the CLI
itself cannot detect. Preflight counts them (current and old slugs, for
proposed and already_done charts) and exits non-zero while any remain. Migrate
them (step 4 gives you each replacement URL), then re-run; --no-references skips
the gate once they are handled.
Non-zero exit means do not run the CLI yet. Pass --decisions whenever a
review happened; flagged charts are excluded (remove them from the CSV, or mark
them SKIP in overrides.csv and re-run step 1).
The skill's output ends here. Send redirects_for_cli.csv and HANDOFF.md
to a Grapher developer, who runs the CLI themselves from the owid-grapher repo.
Never run it — not even --dry-run, which still connects to the production DB.
HANDOFF.md is written for that developer and already carries the commands and
every caveat they need, so don't restate the procedure here or in chat; they
will not have this skill.
Two things stay on our side of the handoff, because the developer won't do them:
cutover_date in migration_log_template.csv and keep it. Analytics cannot
reconstruct it later: prod_semantic.redirects holds no multi_dim_redirects
rows, and once a chart stops being published its whole view history resolves
to chart_id = NULL — retroactively, not just from the cutover. To rebuild a
continuous series, query grapher_views_detailed on the raw grapher column
for the pre-cutover period, the MDIM slug after it, and union the two. Expect
a ~1 week tail of real views on the old URL (redirects fire only on 404 and
Cloudflare serves the cached page meanwhile), and note that query params are
stripped in analytics, so every MDIM dimension collapses into one slug.;-delimited, exactly two columns, a header
tolerated only on line 1, no comment lines anywhere, no duplicate sources,
both fields must start with /. Any violation aborts the CLI's whole run. The
extractor validates its own output and preflight.py re-validates it./explorers/ source.
Both pass the CLI's regex and neither works — see "one bare chart path per row"
above for the mechanism and for what would have to change in grapher.viewConfigId (a chart_configs UUID, = views[].fullConfigId) is what
grapher validates a redirect against; the human-readable view_id
(dim=choice__dim=choice, key-sorted) is for URLs and display. Both are in
every artifact.viewConfigId does not change when the view's content does. Re-exporting
an MDIM looks each view up by its dimension-derived view id and updates that
chart_configs row in place, so the id survives content edits and only ever
changes together with view_id. To tell whether the reviewed rendering still
holds, compare viewConfigMd5 (chart_configs.configMd5) — the target-side
mirror of a source chart's configMd5. Both md5s are in the review
fingerprint and in preflight.py's staleness checks.charts.publishedAt is not the live publication flag. It records the first
publish and stays set after an unpublish, so chart selection and reference grading both read isPublished from
the config instead.{id, catalogPath} per indicator, but older
ones may have catalogPath-only or bare entries — the extractor normalizes all
shapes and batch-resolves against variables.chart_type means map-only (chartTypes: []); the generated column
defaults to LineChart when the config omits chartTypes entirely, so
NULL == NULL is a meaningful tiebreak, not a wildcard.fullConfigId can't be redirect targets and are excluded from
the pool (warned). Same for views with an indicator entry that can't be
resolved to a variable id.overrides.csv; mapping.json is
derived — never hand-edit it.ORDER BY in SQL that selects multi_dim_data_pages.config — the
multi-MB JSON travels with each sorted row and blows the server sort buffer
(MySQL error 1038). Sort client-side (already handled in the extractor).period=nan in
incomes-across-distribution-lis) can be real in the published config —
the target URL works because that IS the dimension value. Don't "fix" it in
the mapping; if it bothers anyone, that's an MDim-authoring fix, done
separately (a redirect created before the fix would break when the slug
changes).After a real run, fold anything the matcher or these docs got wrong back into
this SKILL.md (and check whether the sibling map-explorer-to-mdim skill, including
its own build_review.py, needs the same fix).
none is the matcher's blind spot — when a
reviewer reports a "clear equivalent" the run missed, diff the two sides'
x/size/color slots first (charts.csv vs multidim_views.csv): the y sets being
equal means the miss can only be a slot disagreement, and if it's a decoration
indicator the fix belongs in DECORATION_PATTERN, not in overrides.csv.find-chart-references.
It re-implements the embed count in SQL for read-only use, so any component
the sweep exempts (e.g. all-charts — a topic page's auto-generated index
where a retired chart just drops out) must be exempted there too, or preflight
blocks on a reference the audit rightly never lists.preflight.py --decisions (a decision exported with an empty target
no longer matches the now-targeted row — deliberate, or the flag would silently
drop the freshly forced redirect from the CSV); the forced rows just need a
quick re-approve + re-export.© owid, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 4 other files (scripts) in .claude/skills/map-charts-to-mdim of owid/etl.
Open the folder on GitHubat commit 70c9705
Map Charts To Mdim next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Map Charts To Mdim this skillowid/etl | 159 | — | ~9.2k | Automated safety check: Notes | MIT | |
| Excel and CSV Data Analysisbytedance/deer-flow | 84k | 4 repos | ~2.2k | Automated safety check: Pass | MIT | |
| CSV Data Summarizercoffeefuelbump/csv-data-summarizer-claude-skill | 468 | 2 repos | ~1.4k | Automated safety check: Pass | None | |
| Exploratory Data AnalysisOleafly/Oleafly | 212 | 2 repos | ~3.4k | Automated safety check: Notes | MIT | |
| Raccoon DataanalysisSenseTime-Copilot/raccoon-dataanalysis-skill | 137 | — | ~1.9k | Automated safety check: Pass | None | |
| Football Match Reportricardoherediaj/football-analytics-tutorials | 119 | — | ~2.7k | Automated safety check: Pass | MIT |
bytedance/deer-flow
Analyzes uploaded Excel and CSV files with SQL through DuckDB, producing schema inspections, statistical summaries and exports to CSV, JSON or Markdown.
coffeefuelbump/csv-data-summarizer-claude-skill
Analyzes CSV files, generates summary stats, and plots quick visualizations using Python and pandas.
Oleafly/Oleafly
Perform bounded, local exploratory analysis of explicitly supported scientific files.
SenseTime-Copilot/raccoon-dataanalysis-skill
Raccoon (小浣熊) Data Analysis - Remote code interpreter and data visualization service powered by SenseTime.
ricardoherediaj/football-analytics-tutorials
Build post-match team + player reports (24-chart dashboard, per-player dashboards, stats CSV) from a WhoScored URL.
5zjk5/prompt-engineering
This skill should be used when users need to analyze CSV or Excel files, understand data patterns, generate statistical summaries, or create data visualizations.
owid/etl
Find every OWID surface that references a chart, indicator, MDIM, or explorer — articles (links vs embeds), explorers, narrative charts, data insights, static viz, key-chart slots, MDIM views.
owid/etl
Add a scatter view (with GDP per capita on x) to existing OWID charts via the admin API, mirroring the admin UI's "Add scatter type" defaults, then retire the old standalone "X vs.
owid/etl
Add new survey question codes (e.g. An agent skill from owid/etl.
owid/etl
Build or refresh an OWID static visualization end to end — resolve what data it needs from an old static viz image, an indicator, or a grapher chart; check both the ETL catalog and the producer's…
owid/etl
Take (soon-to-sunset) OWID explorers to redirected MDIMs, end to end.
owid/etl
Check chart or multidim preview on the staging server using a browser.
Categories
Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs. Map Charts To Mdim is an agent skill from owid/etl. Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.
Map Charts To Mdim fits situations like: the user says map charts tagged <X to mdims; propose chart - MDIM redirects; which charts are covered by the new multidim; where do I replace the embedded charts.
Run `npx skills add owid/etl --skill map-charts-to-mdim -a claude-code`. Or copy the skill folder (.claude/skills/map-charts-to-mdim in owid/etl) into .claude/skills/map-charts-to-mdim in your project. Claude Code loads it when a task matches its description.
Run `npx skills add owid/etl --skill map-charts-to-mdim -a codex`. Or copy the skill folder (.claude/skills/map-charts-to-mdim in owid/etl) into .agents/skills/map-charts-to-mdim in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add owid/etl --skill map-charts-to-mdim -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/map-charts-to-mdim, .gemini/skills/map-charts-to-mdim, .github/skills/map-charts-to-mdim and .opencode/skills/map-charts-to-mdim in your project.
Going by SKILL.md and its folder, Map Charts To Mdim needs Python for the scripts in its folder and the command-line tools its instructions call (python). Our summary lists: Python 3.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Map Charts To Mdim is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.2k tokens (SKILL.md is roughly 37k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Map Charts To Mdim: Excel and CSV Data Analysis (bytedance/deer-flow, 84k stars), CSV Data Summarizer (coffeefuelbump/csv-data-summarizer-claude-skill, 468 stars), Exploratory Data Analysis (Oleafly/Oleafly, 212 stars) and Raccoon Dataanalysis (SenseTime-Copilot/raccoon-dataanalysis-skill, 137 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
owid (a GitHub organization) maintains it in owid/etl, which has 159 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on October 10, 2026.
Source: owid/etl on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.