Agent skill

Create Static Viz

by owid in 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…

MITAuto-check passedData & Analytics

Install Create Static Viz

skills CLI
$ npx skills add owid/etl --skill create-static-viz -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install owid/etl create-static-viz --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/owid/etl.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/create-static-viz .claude/skills/create-static-viz && rm -rf skills-src

Use ~/.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/

Facts

Skill name
create-static-viz
GitHub stars
158
Token cost
~8.3k tokens
SKILL.md length
4,860 words
Files
11 (incl. scripts)
Skills in repo
35
Repo updated
First seen
Licence
MIT

At a glance

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…

  • Works in 9 steps: Resolve the input to data → Check for newer data. Always, even when… → One checkpoint, before writing anything → …
  • The user asks to refresh this static viz
  • SKILL.md covers The project's rules, Author credit, Inputs and Sketch mode — a data file in,…, plus 9 more sections
  • Runs Python scripts from its folder; calls python, make and git; reaches ourworldindata.org

What it does

Create Static Viz is an agent skill from 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 own site for a newer release and route to /create-dataset or /update-dataset when one exists; write the viz://static matplotlib step that emits Figma-ready SVG and PNG at the static-chart templates' proportions; then hand off to /owid-staff:create-figma-chart. Sketch mode: prototype a NEW static viz from a…

Its SKILL.md is about 8.3k tokens, which your agent loads only when the skill is triggered. The skill folder holds 12 other files, including scripts (for example `TEMPLATES.md`, `reference/GOTCHAS.md` and `reference/SKETCHING.md`).

It sits in Data & Analytics, covering Data visualization, Data pipelines and ETL and DataFrames. It works with Figma, Matplotlib and Microsoft Excel. The repository describes itself as: A compute graph for loading and transforming OWID's data. The licence is MIT.

When your agent uses it

  • The user asks to refresh this static viz
  • Remake this chart as a static image
  • Create a static viz
  • Sketch a static viz from this CSV

Example prompts

  • “refresh this static viz”
  • “remake this chart as a static image”
  • “create a static viz”
  • “/create-static-viz”

Requirements

  • Python 3

Workflow steps

9 steps, taken from the step headings in SKILL.md.

  1. Resolve the input to data
  2. Check for newer data. Always, even when it is already in ETL
  3. One checkpoint, before writing anything
  4. Write the viz://static step
  5. Render, verify, and look at it
  6. Iterate with the user
  7. Hand off to /owid-staff:create-figma-chart
  8. Record the Figma handoff in the step's docstring
  9. PR and the review chain

What it can do on your machine

Read from SKILL.md and the folder at commit bf5dc8e. It shows what the files ask for, not the result of running them.

  • Tool permissions

    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.

  • Runs code

    Ships 5 files in scripts/ (Python), which the agent can run.

    Shell commands in SKILL.md call:

    • python
    • make
    • git

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • ourworldindata.org

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Create Static Viz loads about 8.3k tokens when it runs. Until then it costs about 250 tokens; SKILL.md has 4,860 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~250
When it runs · the whole SKILL.md, loaded when a task matches
~8.3k

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.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated 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.

SKILL.md

The full file from owid/etl at commit bf5dc8e, republished under its MIT licence (© owid). 4,860 words, ~8,333 tokens.

Download SKILL.mdSave it as .claude/skills/create-static-viz/SKILL.md (or your agent's skills folder). This skill also uses 10 other files; get the full folder from GitHub.
name
create-static-viz
description
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 own site for a newer release and route to /create-dataset or /update-dataset when one exists; write the viz://static matplotlib step that emits Figma-ready SVG and PNG at the static-chart templates' proportions; then hand off to /owid-staff:create-figma-chart. Sketch mode: prototype a NEW static viz from a CSV/Excel/parquet file (or a quick data pull) in a gitignored scratch dir, iterate visually in matplotlib and Figma, then promote it to a viz://static step. Trigger when the user asks to "refresh this static viz", "remake this chart as a static image", "create a static viz", "sketch a static viz from this CSV", "brainstorm a chart from this data", pastes an old static viz image or filename and asks for a better version, or picks up a viz from the static-viz refresh queue.
metadata.internal
true
metadata.owner
paarriagadap

Create or refresh a static visualization

Joins the three halves of a static-viz refresh that are otherwise separate: getting the data into ETL at a current vintage, drawing it in an viz://static step whose SVG a designer can actually pick up, and getting that SVG into the Charts file.

Model check: the session context names the running model. On Fable, recommend re-running on Opus (or Sonnet for a mechanical re-render) before starting, and continue only on the user's say-so — same rule as /owid-staff:create-figma-chart.

Paired skill — an update here may oblige an update there, and the reverse. /owid-staff:create-figma-chart owns everything that happens inside Figma, and this skill hands off to it at Step 7. The two share a contract that lives half in each file, so when you change something on this list, check the other skill in the same session and update it too — or state explicitly that you checked and no change was needed. Neither side may drift silently; a stale cross-skill fact is how a later run re-derives geometry by trial and error. That skill now lives in owid/skills-private (the owid-staff plugin, auto-installed here via .claude/settings.json); invoke it as /owid-staff:create-figma-chart.

Shared factOwnerConsumed by
Template geometry — node ids, sizes, band top, footer startsTEMPLATES.mdboth
The content box and the band a chart is fitted intoTEMPLATES.mdboth
Node naming (gids) this step emits, and frame proportionsthis skillthat skill's Steps 1/3/7–8
Which text slots this step fills vs. leaves to the templatethis skillthat skill's Step 6
Type and palette — this step sets neitherthat skillthis skill defers to it
The design vocabulary (per chart type, labeling, colors)that skill's GUIDELINES.mdboth
The sketch dir ai/static-viz-sketches/<slug>/, what its SVG carries, and the Figma handoff docstring section that records the pagethis skill (reference/SKETCHING.md)that skill's sketch mode, local-SVG route
The sketch marker — page … [sketch], frame <slug>--sketch; no sketch carries the bare website slugthat skillthis skill reports it
Finalize mode as the re-entry point once a sketch is promotedthat skillthis skill's Sketch mode, §8

The asymmetry worth remembering: this skill owns the data, the geometry and the proportions; that one owns the type and the palette. A change that crosses that line belongs in both files. In particular, if you change what the step emits — node naming, frame proportions, which text slots it fills — check whether that skill's Step 1/3/7 notes on local SVGs still hold.

Its GUIDELINES.md is where the visual vocabulary this skill defers to actually lives — the per-chart-type rules, direct labeling in place of legends, the OWID palette, the annotation and reference-line conventions. Read the section for your chart type before choosing a form or deciding what to label: the point is not to style anything here, it is to avoid emitting a structure the Figma pass then has to undo (a legend that should have been direct labels, a category count that cannot be labeled in place). That file also indexes the design team's DI Chart Library (pltrHXyVLg2XaNq4AvxPaK, read-only) — 288 finished charts on 2026-10-01, filed by chart type (dumbbell plots now have a page of their own; GUIDELINES.md keeps the current per-page counts), which is the closest thing to precedent for whatever you are about to draw.

Two companions in this directory:

  • TEMPLATES.md — the static-chart template geometry, measured off the Charts file. Read it before laying anything out; don't re-derive it through MCP calls.
  • scripts/verify_static_viz.py — mechanical check that the emitted files honor the Figma handoff contract. Run it before showing anyone the chart.

The step-by-step detail lives in reference/ and is read at that step:

ReadWhenCovers
reference/WRITING-THE-STEP.mdStep 4The handoff contract, grapher's axis and tick treatment, encoding diagrams, desktop/mobile pairing, text slots, labelling many categories, Figma-surviving anchors, the assertions to write.
reference/GOTCHAS.mdIts Data section at Step 1, before any column is used; the rest on an error, or grep by symptomData, layout and workflow pitfalls.
reference/SKETCHING.mdSketch mode, instead of Steps 1–4Data pull, scaffold, render, verify, the Figma sketch handoff, iterating, promotion to a step.

Size budget, enforced by --structure: this spine under 34 KB, TEMPLATES.md under 25 KB. (Raised from 30 KB on 2026-09-17 to land Sketch mode and its contract rows, and to 34 KB on 2026-10-05 to land the published-original handoff in Step 7 — the spine sat 11 bytes under 33 KB. The discipline is unchanged.) Both are read on every run, so a paragraph added here costs every future viz — new detail belongs in the reference file for its step. After editing any doc in this skill:

bash
.venv/bin/python .claude/skills/create-static-viz/scripts/verify_docs.py --structure
# and after moving text between files, prove nothing was dropped:
.venv/bin/python .claude/skills/create-static-viz/scripts/verify_docs.py --against <ref>

When you do make a Figma MCP call, batch it. A call costs twice — the network hop to the hosted connector and the model turn around it. The hop is environment-specific and the cloud is the faster side of it: get_screenshot 7.8–9.9 s from a sandbox against 12.5–20.5 s locally, use_figma 0.70 s against 3.5–5.8 s. The turn is not environmental — it tracks the work in it, and identical light probes measured 2.8 s in a sandbox against 3.7 s locally. (A ~12 s cloud turn was claimed here once; it came from turns doing real chart work, so it is the heavy-turn cost either side.) Either way, a handful issued one at a time is the difference between seconds and minutes. Batching pays about the same in both: the connector serves concurrent calls — eight screenshots in one message measured 4.1× faster than serially, and ten reps of a fixed six-call probe a side put it at ≈4.0× in both environments (4.00× cloud, 3.84× local, six in flight every time) — and admits about four or five at once, so put independent calls in one message, 4–6 at a time. A batch's wall is first call + rate × (n−1) — measured at 9.2 s + 0.75 s per extra screenshot in a cloud session and 11.7 s + 2.1 s locally — and that marginal cost is the stopping rule. Those are screenshots; batching use_figma never compresses its calls (one plugin context per file) and pays only by collecting turn gaps — still worth it, and worth more in the cloud, where the cheap call makes the turn 80% of the cost rather than 46%. Reads always; writes only when they target different pages. If the Figma tools arrive deferred — a harness setting, not an environment — load the ones you need in a single ToolSearch, taking the prefix from your own session's tool list rather than assuming one; skip that where they are already loaded. /owid-staff:create-figma-chart → Round-trip budget has the full rule and the list of what is genuinely serial.

What this skill does not decide: colors, fonts, background, the logo, and any visual treatment the template provides. Those are applied in Figma. The ETL step owns the data, the structure (which text slots exist, in what order), the proportions, and the axis conventions. Setting a cream background or a serif title in matplotlib is work that will be thrown away.

The project's rules

Policy, quoted from the parent issue (owid/owid-issues#2459, and identically in the earlier #2278). These are not this skill's inventions and are not negotiable here.

Scope. A refresh is either a data update plus light visual tweaks (the common case) or visual polish only (when the data is already current).

We should not do deep redesigns. Aim to reproduce the same visualization with the same broad design choices.

So the default is to rebuild the same chart, better — not to redesign it. When the existing design has a real defect — an encoding that misleads, overlapping translucent fills that produce a color meaning nothing, a caption that misstates what the data shows — a departure can be the right call. But it then stops being a refresh, and needs naming as such: say plainly that it exceeds the scope rule, say which defect justifies it, and get design sign-off on the departure rather than letting it pass as polish.

Quality bar.

  • All numbers match the source.
  • As much as possible, make visualizations reproducible so future updates are easier (and transparent).
  • Design consistency: Marwa should sign off on every new static viz.
  • Mobile: discuss with Marwa whether a mobile version is feasible; otherwise, make the desktop version as readable as possible on mobile.

The second line is why this skill exists: an viz://static step is the reproducibility requirement, met. The third is not optional — @mrwbkrm signs off on every one. The fourth means a mobile version is a question to ask, not a default to assume.

Pace. Work in progress stays at roughly one or two open child issues. Don't start a new refresh to avoid finishing one.

Author credit

The license line carries the credit, and who appears on it depends on how much changed:

What changedCredit
Data updated, design broadly preservedthe original author, and whoever is doing the refresh
Design changed considerablywhoever is doing the refresh, alone

"Whoever is doing the refresh" is the person running this skill — not a fixed name. Resolve it from git config user.name and confirm it at the Step 3 checkpoint, following the repo convention of crediting the human directing the work and asking when it is ambiguous. Never carry a name over from a previous run of this skill, or from the AUTHOR constant of another step you copied from.

The original author is not always recorded anywhere obvious; static_viz_popular.csv carries viz_authors / viz_authors_source, and the old image's own footer usually states it. When the call between the two rows is genuinely close, ask — it is someone's credit.

This is also a useful test of whether the scope rule above has been crossed. If the honest credit drops the original author, the design changed considerably, and that is the case needing the sign-off conversation rather than a quiet ship.

Inputs

Any one of:

  • An old static viz — the image, its filename, or the page it appears on.
  • An indicator — a catalogPath, or a description of one.
  • A grapher chart — live URL, staging URL, admin edit URL, bare slug, or chart id.
  • Just a description of the chart wanted, plus a source.
  • A data file — CSV, Excel or parquet, for a sketch of a new viz: see Sketch mode below.
  • "Sketch", "brainstorm" or "prototype" plus an indicator or chart — pull its data to a CSV first, then Sketch mode.

Optionally: the article or topic page the viz belongs to, and a reference page in the Charts file to work like (/owid-staff:create-figma-chart has a whole mode for that).

Sketch mode — a data file in, no ETL until the visuals settle

For a new static viz that starts from data rather than from an existing viz. The input decides the mode — a data file, or a sketch/brainstorm/prototype ask, is a sketch; say so in one line, with what it skips, and proceed. Read reference/SKETCHING.md and follow it instead of Steps 1–4. In short: scripts/new_sketch.py scaffolds ai/static-viz-sketches/<slug>/sketch.py (gitignored) in the exact shape of a viz://static step, around a copy of the data file; render it with .venv/bin/python, run the verifier and read the PNG as in Step 5, iterate (variants are extra LAYOUTS keys), hand the SVG to /owid-staff:create-figma-chart's sketch mode and iterate there too; once the visuals settle, scripts/promote_sketch.py turns the sketch into a real step. Skipped — say so to the user every time: the branch, worktree and PR, the DAG entry, Step 2's newer-data check, the tracker question, the review chain. Promotion runs all of them: Steps 1–2, then 5–9, plus that skill's finalize mode on the sketch frame. Not skipped: the verifier, reading the PNG, and offering promotion and finalize at the end of every sketch reply — people may not know the checks exist. A Data Insight image is not a static-viz sketch — it is sketched in that skill directly, from the grapher chart.

Step 1 — Resolve the input to data

Reuse the existing resolver rather than writing another one:

bash
.venv/bin/python .claude/skills/edit-faust-metadata/scripts/resolve_target.py <reference> [--json] [--no-db]

It takes a live/staging/admin URL, a bare slug, a chart id, or an indicator catalogPath, reports the chart's variables with their catalogPaths, and names the candidate ETL step files — including a warning when the grapher catalogPath's version differs from what is on disk.

In a cloud session, run it with --no-db first — only the DB half is unavailable. Parse-only mode never calls read_sql, so it works in a sandbox: it identifies what the reference is, preserves the dimension query params, and names the candidate ETL step files for an indicator or MDim catalogPath. Do that before reaching for anything else. What --no-db cannot give you is the DB half — a bare slug stays chart-or-mdim (needs DB), and the chart's variables and their catalogPaths need MySQL.

For that half, don't wait on staging. A cloud sandbox has no MySQL and staging is on Tailscale, so the DB path reports "Staging server … is not reachable. Run etl pr first or wait for the staging build to finish" — which invites waiting for something that will never arrive here. Don't wait, and don't run etl pr for it. Fall back to the read-only routes in cloud-sandbox.md — https://ourworldindata.org/grapher/<slug>.config.json for a published chart, the public Datasette for variables and chart_dimensions — or ask the user to run the resolver locally and paste the output. This is a fallback for one environment, not a replacement: use the resolver with its DB wherever MySQL is reachable, since it does strictly more than the fallbacks do.

From an old static viz image, two routes, neither of which the popularity CSV can do alone:

  1. The grapher static_viz table carries a grapherSlug column. That gives a slug, and the slug goes through the resolver above. Note this table is not mirrored to the public Datasette, so query a staging DB or the local dev DB (see /query-grapher-db) — which means a cloud session cannot reach it either, and the slug has to come from the user.
  2. ai/static_viz_popularity/static_viz_popular.csv gives rank, the pages it appears on, views, authors and tags — useful context, but it has no slug or indicator column, so it cannot reach the data by itself.

If neither resolves it, ask. Guessing which dataset an old hand-drawn image was built from is how you rebuild the wrong chart.

The project tracks which viz is claimed, parked or already done in a shared tracker, and this skill deliberately does not read or write it — ask instead. If the user has not already said where this viz stands, ask before doing any work: a viz someone else is mid-way through, or one already finished, is worth an interruption rather than a duplicate.

Read reference/GOTCHAS.md → Data before you use a single column. Those checks are pre-flight, not post-mortem: a column whose name means something other than what it holds, an unasserted splice, a framing that stops holding partway through a series, and an over-claim repeated across the metadata each render as a plausible chart with nothing to grep for. They are how a run that raises no exception still publishes wrong numbers.

Then report what you found: which dataset, which version, and how many charts use it.

Step 2 — Check for newer data. Always, even when it is already in ETL

Two sides, and both are needed. The ETL side tells you whether a newer version in our catalog exists; only the producer side tells you whether newer data exists at all.

ETL side.

python
from etl.version_tracker import VersionTracker
df = VersionTracker().steps_df   # update_state, update_period_days, days_to_update, n_charts

update_state is one of Unknown / No updates known / Outdated / Minor update possible / Major update possible / Not yet used. Outdated means a newer version of that step already exists in the DAG — the viz is pointing at a stale vintage. Also read each garden dep's snapshot .dvc for date_published and date_accessed, and the garden .meta.yml for update_period_days.

Be clear about what this is: days_to_update is step version date + update_period_days, a proxy. VersionTracker's own docstring says so. It is DAG version arithmetic, not knowledge of the producer's release calendar.

Producer side — this is the part that needs the internet. Fetch the origin's url_main and search for the current release. Report what the producer publishes now against what the snapshot captured.

Version labels prove nothing. Producers replace the published file — and the codebook — without bumping a stated version and without a changelog. Compare the hosting platform's file-modification dates or hashes (an OSF API date_modified, an HTTP Last-Modified, a checksum) against the previous snapshot's date_accessed and md5. An unchanged version label is not evidence of unchanged data.

Two more traps worth carrying from /update-dataset:

  • date_published and the year inside citation_full never update themselves. etl update clones the previous .dvc verbatim except date_accessed. Re-check both against the source.
  • Producer prose can lag the producer's own tables. Trust the data over the landing page.

Then route, and never do the ingest or update inline:

FindingAction
Not in ETL at allhand off to /create-dataset
In ETL, newer release exists upstreamhand off to /update-dataset
In ETL, update_state is Outdateda newer ETL version exists — repoint the viz at it
In ETL and currentsay so explicitly, and proceed

"In ETL and current" is a real, reportable finding — say it rather than staying silent, so the user knows the check happened.

Step 3 — One checkpoint, before writing anything

Put the whole proposal in front of the user at once, and get an explicit go-ahead:

  • the dataset and its vintage, plus what Step 2 found upstream
  • what the viz shows, and how it differs from the image it replaces
  • which template(s), and desktop and/or mobile
  • the author credit and the license line — the templates leave Licensed under CC-BY by the author [Name of author]. Propose the name(s) per the Author credit rules above, resolving the refresher from git config user.name, and say which of the two cases you think applies — that is also the moment to say out loud if the design change looks big enough to have crossed the scope rule. Whether CC-BY is correct cannot be inferred either: a source under CC BY-NC-SA is not automatically redistributable as CC-BY, so ask rather than filling the slot.

This mirrors /owid-staff:create-figma-chart's single-checkpoint rule, for the same reason: everything after here is expensive to redo.

Show full SKILL.md (1,870 more words)Show less

Step 4 — Write the viz://static step

Read reference/WRITING-THE-STEP.md for this step.

Everything about authoring the step: the Figma handoff contract the emitted files have to satisfy, grapher's axis and tick treatment, encoding diagrams, pairing desktop with mobile, the template's text slots, labelling many categories, anchoring labels so they survive Figma, and the assertions to write.

Step 5 — Render, verify, and look at it

bash
.venv/bin/etlr viz://static/<ns>/<version>/<short_name>

A static step only writes the PNG and SVG next to the recipe, so named by its URI it runs fully without --grapher (a pattern such as etlr population would select it only with the flag).

From a fresh worktree, give it its own .venv before rendering. A worktree starts without one, and borrowing the main checkout's is a trap: etl is installed there editable via a .pth holding the main checkout's path, so paths.BASE_DIR resolves to the main checkout whatever your cwd is. etlr then loads the main checkout's copy of the step and writes the PNG/SVG next to it — the run reports Finished, your worktree's files never change, and that reads exactly like a no-op render.

bash
make .venv                             # uv sync --all-extras --group dev; the pre-commit hook also does this
ln -s /path/to/main/checkout/data data # gitignored; the built deps only exist in the main checkout
.venv/bin/etlr viz://static/...

Confirm before trusting a render: .venv/bin/python -c "from etl import paths; print(paths.BASE_DIR)" should print the worktree, and the log's "Saved chart to" path should too. Compare output mtimes against the step's own — an output older than the source means you are reading a stale file.

Editing the step's .py is enough to trigger a rebuild on its own, so you rarely need to force anything. For the narrow case where nothing in the repo changed but you still need to re-run, use --force --only — never --force alone, which would also re-run every upstream dependency. --only is safe here because the deps are already on disk from the run you are repeating.

Then, in this order:

  1. Run the verifier, and always pass the data layers — without --expect-gid the naming check only proves some node was named, which a figure with a named title and an unnamed line satisfies:

    bash
    .venv/bin/python .claude/skills/create-static-viz/scripts/verify_static_viz.py <step-dir> \
        --template <name> --expect-gid <data-layer> [--expect-gid <data-layer> ...]
  2. Read the PNG. The verifier cannot see a collision, a widow, or a label sitting on a curve. Every layout bug in this skill's Gotchas was found by looking.

Step 6 — Iterate with the user

Show the render. When a design choice is genuinely open, measure the options and offer the numbers, not adjectives — see the panel-aspect gotcha under Layout for why.

Step 7 — Hand off to /owid-staff:create-figma-chart

Give it the local SVG path. That skill's Step 1/3 cover the local-file route: there is nothing to export, and none of the .metadata.json text sourcing applies because the text is already in the file. Its upload_assets import is already file-based.

The one adaptation: its Steps 7–8 look up grapher's node names (connectors, horizontal-grid-lines, datapoints__<Entity>). Ours are the gids from Step 4 — hand over the naming scheme along with the file.

On a refresh, hand over the published original too, to sit leftmost on the page. Reviewers judge the new chart against what readers see now, not against the step's last render. Fetch it with .venv/bin/python .claude/skills/create-static-viz/scripts/fetch_original_image.py <filename>. It reads the images table and saves whatever format Cloudflare serves, usually JPEG. Upload it as a raster, set it to the desktop frame's width, and give it the layer name the script prints. It stays when old versions are cleared.

Three of that skill's geometry rows only mean anything once the import is cropped — and then they mean everything. box-alignment, gap and margins read the chart frame's box, and an import arrives the size of the SVG canvas, which is the artboard: uncropped they report left −16 / right +16, negative insets and the canvas rectangle, every time, on a page that is correct. restyle_static_import.js removes both causes — it strips matplotlib's frame-sized background patch (which paints nothing but sets the bbox) and crops the frame to its painted ink, snapping a side that lands within a pixel of the content column. After that, treat those rows as live: box-alignment must be exact and margins must pass — a failure there is real, not this route's noise.

gap is the one to read rather than chase: it measures against the header frame's bottom and the footer frame's top, which a band derived from line-box arithmetic misses by a few px, so a page asserting a 14 px inset can read 18.35 / 17.01 against the 12–16 target. Report both numbers and name the datum the step used — TEMPLATES.md has the measurements and which to prefer.

Use grapher's OWN names for the data layers wherever the shape matches, because that skill's Step 8c checks key off them and skip when they are absent. Measured on a real DI frame drawn by a local generator — nine panels, nine series lines, 27 direct labels, all correct — seven rows skipped for want of a name: series-weight, furniture-weight, polylines, label-contrast-on-background, colour-vision and grayscale-seams, and the palette read the chart as a single-colour palette of the panel fill, never seeing the series colour. Nothing failed and nothing was silently passed — each row named what it could not find — but the coverage behind "no mechanical row failed" was far thinner than the same sentence on a grapher import. So emit gid="line__<Entity>" for a series line, label__<Entity> for a direct label, and horizontal-grid-lines for the gridline group, rather than inventing a scheme. These are prefixes, load-bearing as such — see GOTCHAS.md. Renaming in Figma afterwards works but does not survive a re-import; the gid does.

Note what a gid actually produces: matplotlib writes <g id="label__Ghana"><text/></g>, so Figma imports a GROUP named label__Ghana holding a TEXT named Ghana — the name lands on the wrapper, never on the painted node, and a series arrives with Figma's own "Clip path group" wrappers between the named group and the vector. That is fine and expected; /owid-staff:create-figma-chart's rows resolve through the naming ancestor. Set the gid on the artist and don't try to name the glyph itself.

Step 8 — Record the Figma handoff in the step's docstring

Once the Figma page exists, write the handoff back into the step's module docstring. The bar is that a later session can redo the whole thing from this file alone — a different person, months on, with none of this conversation and no memory of the run. Not notes on what was done: a recipe.

That bar is the point of the step. Everything about the handoff lives in one of two places — this file, or a session transcript that vanishes. Whatever is only in the transcript gets re-derived by trial and error at the next data update, which is how the numbers drift and how a deliberate design decision quietly becomes an accident. So write it down even when it feels obvious today.

Record, concretely:

  • Where. File name and key, the page name and where it sits in the page order, each frame's name, and the template node id and size it was cloned from. Names, not just a link: a node-id is stable but says nothing about what to reproduce.
  • The import mechanics that are not obvious. How the SVG gets in (upload_assets + POST, never createNodeFromSvg), that the wrapper frame is binned and why, and the scale factor with its derivation — plus its self-correcting form, so a reader can check the number rather than trust it.
  • Every text slot: what fills it and which parts are bold. A table, because setting characters flattens the mixed weights the templates ship.
  • Any position that is derived rather than taken from the template's fixed y, with the arithmetic.
  • Every color, as its library style name and key, plus how anything derived from it (band tints) is computed. A hex alone is unreproducible — nobody can tell whether it came from the palette.
  • The in-plot restyle: each type rank with its size and weight, and the anchor rule per label family.
  • The fit, and whether a rescale is wanted (usually not, and why not).
  • The audit numbers to expect, so the next run can tell success from a near-miss.

Two things that belong here and are easy to leave out. The order operations must run in where one depends on another settling — widths settle on the next call; a coordinate patch after a fit uses anchors the fit has already invalidated. And any deviation accepted on purpose — an off-ladder size, a grayscale seam that does not gate — with its reason, so a later audit reads it as a decision rather than a defect.

Write it in the imperative, as the step's own reference. If you catch yourself writing "we changed X", rewrite it as "set X to Y, because Z": the reader wants to reproduce the state, not the history.

Step 9 — PR and the review chain

The branch, worktree and draft PR already exist from Step 4. Let the pre-commit hook run make check when you commit — the step is ordinary ETL code and has to be formatted, linted and typechecked like any other. Then commit the step plus its committed PNG/SVG, push, and fill in the PR body — whose first line is the attribution blockquote, > _Written by Claude <model name> — @<handle> at the wheel._, because the body goes out under a human's identity. It is required on every comment you post to the PR afterwards too, replies to the review included. Then /babysit-pr for the Codex round. Brief the babysitter with the deliberate decisions (see the babysitter gotcha under Workflow).

The code review is only the first of several. The project defines the rest, and they are people, not checks — from #2459's workflow, with the parts this skill touches in bold:

  1. Check for new data and identify where the image is used — Steps 1–2 here, plus a /find-chart-references sweep for every page that renders it. Static viz is classified embed, so a URL redirect does not fix it; each surface is a manual swap.
  2. Find dependencies — other charts on the same page, or other charts on the same data, that would now disagree with the refreshed one.
  3. Consult the author — required when the image sits on an article. Topic pages are evergreen: update directly, and update the surrounding text. For an article, whether to publish an updated version at all is the author's call.
  4. Child issue opened (Bertha).
  5. Pull the data, rebuild, import to Figma — Steps 4–7 here.
  6. Review as needed (Bertha) → design review with @mrwbkrm, mandatory → final edits → final review with @edomt.
  7. Upload the refreshed image under a new filename and repoint the references. The API rejects a duplicate name, and the old file must stay reachable for anything still pointing at it.
  8. Accompanying text edited (Bertha and the author).

Report which of these are done and which are outstanding — the surrounding prose in particular tends to be forgotten, and a corrected chart under an uncorrected caption is worse than neither.

Finally, remind the user to set the viz's status in the tracker themselves, and give them the PR link to record against it. This skill never reads or writes the tracker — it is a shared team database, the person running the refresh knows the state of their own queue, and a status is a claim about human intent that an automated flow should not be making.

© owid, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 10 other files (scripts) in .claude/skills/create-static-viz of owid/etl.

  • SKILL.md
  • TEMPLATES.md
  • reference/GOTCHAS.md
  • reference/SKETCHING.md
  • reference/SUPERSEDED-LOGO-GENERATION.md
  • reference/WRITING-THE-STEP.md
  • scripts/fetch_original_image.py
  • scripts/new_sketch.py
  • scripts/promote_sketch.py
  • scripts/verify_docs.py
  • scripts/verify_static_viz.py

Open the folder on GitHubat commit bf5dc8e

Compare with similar skills

Create Static Viz 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.

Create Static Viz compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Create Static Viz this skillowid/etl158—~8.3kAutomated safety check: PassMIT
Paper FiguresEvoScientist/EvoSkills4751 repos~4.4kAutomated safety check: PassApache-2.0
Raccoon DataanalysisSenseTime-Copilot/raccoon-dataanalysis-skill137—~1.9kAutomated safety check: PassNone
CSV Data Analysis5zjk5/prompt-engineering127—~2.6kAutomated safety check: PassNone
Hybrid-Engine Data Analysiscode-yeongyu/oh-my-openagent70k—~1.4kAutomated safety check: PassCustom licence
Exploring Dataoaustegard/claude-skills150—~1.7kAutomated safety check: PassMIT

Similar skills

  • Paper Figures

    EvoScientist/EvoSkills

    A skill your agent uses to produce standalone, publication-ready PNG graphics and reproducible matplotlib scripts from tabular data (CSVs or DataFrames).

    475 GitHub starsUsed in 1 repo~4.4k tokens
    Data & AnalyticsAuto-check passed
  • Raccoon Dataanalysis

    SenseTime-Copilot/raccoon-dataanalysis-skill

    Raccoon (小浣熊) Data Analysis - Remote code interpreter and data visualization service powered by SenseTime.

    137 GitHub stars~1.9k tokensUpdated 6 mo ago
    Data & AnalyticsAuto-check passed
  • CSV Data Analysis

    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.

    127 GitHub stars~2.6k tokensUpdated 22 days ago
    Data & AnalyticsAuto-check passed
  • Hybrid-Engine Data Analysis

    code-yeongyu/oh-my-openagent

    Analyzes CSV, Parquet and JSON data with DuckDB, Polars, numpy and matplotlib, preferring a persistent kernel over repeated one-shot processes.

    70k GitHub stars~1.4k tokensUpdated today
    Data & AnalyticsAuto-check passed
  • Exploring Data

    oaustegard/claude-skills

    Exploratory data analysis. An agent skill from oaustegard/claude-skills.

    150 GitHub stars~1.7k tokensUpdated today
    Data & AnalyticsAuto-check passed
  • ModelViz Scientific Plots

    hrdZhu/modelviz-skill

    Turns your CSV or Excel data and a plain-language request into a publication-style scientific chart by adapting a catalog template, then checks and repairs it.

    286 GitHub stars~3.6k tokensUpdated 2 mo ago
    Data & AnalyticsAuto-check passed

More from owid/etl

All 35 skills in this repo
  • 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.

    158 GitHub stars~4.9k tokensUpdated today
    Auto-check passed
  • 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.

    158 GitHub stars~19k tokensUpdated today
    Auto-check passed
  • Add new survey question codes (e.g. An agent skill from owid/etl.

    158 GitHub stars~11k tokensUpdated today
    Auto-check: notes
  • Propose redirects from (soon-to-sunset) grapher charts to the matching views of published MDIMs.

    158 GitHub stars~9.6k tokensUpdated today
    Auto-check: notes
  • Take (soon-to-sunset) OWID explorers to redirected MDIMs, end to end.

    158 GitHub stars~7.3k tokensUpdated today
    Auto-check: notes
  • Check chart or multidim preview on the staging server using a browser.

    158 GitHub stars~1.2k tokensUpdated today
    Auto-check passed

Questions about Create Static Viz

What does Create Static Viz do?

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…. Create Static Viz is an agent skill from 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 own site for a newer release and route to /create-dataset or /update-dataset when one exists; write the viz://static matplotlib step that emits Figma-ready SVG and PNG at the static-chart templates' proportions; then hand off to /owid-staff:create-figma-chart.

When should I use Create Static Viz?

Create Static Viz fits situations like: the user asks to refresh this static viz; remake this chart as a static image; create a static viz; sketch a static viz from this CSV.

How do I install Create Static Viz in Claude Code?

Run `npx skills add owid/etl --skill create-static-viz -a claude-code`. Or copy the skill folder (.claude/skills/create-static-viz in owid/etl) into .claude/skills/create-static-viz in your project. Claude Code loads it when a task matches its description.

How do I install Create Static Viz in Codex?

Run `npx skills add owid/etl --skill create-static-viz -a codex`. Or copy the skill folder (.claude/skills/create-static-viz in owid/etl) into .agents/skills/create-static-viz in your project. Codex loads it when a task matches its description.

Can I use Create Static Viz in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add owid/etl --skill create-static-viz -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/create-static-viz, .gemini/skills/create-static-viz, .github/skills/create-static-viz and .opencode/skills/create-static-viz in your project.

What does Create Static Viz need to run?

Going by SKILL.md and its folder, Create Static Viz needs Python for the scripts in its folder and the command-line tools its instructions call (python, make and git). Our summary lists: Python 3.

Does Create Static Viz access the network?

SKILL.md names 1 domain. In commands or code: ourworldindata.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Create Static Viz safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. 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.

What licence does Create Static Viz use?

Create Static Viz is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Create Static Viz use?

About 8.3k tokens (SKILL.md is roughly 33k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Create Static Viz?

Skills that share tags, products or a category with Create Static Viz: Paper Figures (EvoScientist/EvoSkills, 475 stars), Raccoon Dataanalysis (SenseTime-Copilot/raccoon-dataanalysis-skill, 137 stars), CSV Data Analysis (5zjk5/prompt-engineering, 127 stars) and Hybrid-Engine Data Analysis (code-yeongyu/oh-my-openagent, 70k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Create Static Viz?

owid (a GitHub organization) maintains it in owid/etl, which has 158 GitHub stars. The repository holds 35 skills in this directory. The repository was last updated on October 8, 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.