/visualization-settings
Choosing a card's `display` and authoring `visualization_settings` for question_write and patch_dashcard — which chart fits which data shape, the output-column-name rule, minimum keys per chart family, map regions, the column_settings JSON-string-key footgun. Triggers — "make
Claude Code
$ npx -y skills add metabase/metabase --skill visualization-settings --agent claude-code
Codex
$ npx skills add metabase/metabase --skill visualization-settings -a codex -y
Cursor
$ npx skills add metabase/metabase --skill visualization-settings -a cursor -y
Gemini CLI
$ npx skills add metabase/metabase --skill visualization-settings -a gemini-cli -y
GitHub Copilot
$ npx skills add metabase/metabase --skill visualization-settings -a github-copilot -y
OpenCode
$ npx skills add metabase/metabase --skill visualization-settings -a opencode -y
Windsurf
$ npx skills add metabase/metabase --skill visualization-settings -a windsurf -y
SKILL.md
visualization-settings.SKILL.mdname: visualization-settings
description: Choosing a card's `display` and authoring `visualization_settings` for question_write and patch_dashcard — which chart fits which data shape, the output-column-name rule, minimum keys per chart family, map regions, the column_settings JSON-string-key footgun. Triggers — "make this a bar/line/pie chart", "what chart should I use", "format as currency", "the card renders as a table instead of a chart", "conditional formatting", "region map".
Visualization: pick the chart, then set it
Two fields, set through `question_write` and mergeable per dashcard via `dashboard_write`'s `patch_dashcard`:
- **`display`** — `table`, `bar`, `line`, `area`, `row`, `pie`, `scalar`, `smartscalar`, `combo`, `pivot`, `funnel`, `map`, `scatter`, `waterfall`, `progress`, `gauge`, `sankey`. Nothing else: an unknown value (`bargraph`, `histogram`, `number`) is accepted and renders nothing; `scalar` **is** the Number viz.
- **`visualization_settings`** — a map keyed per display (`graph.*`, `pie.*`, `table.*`, …). **Nothing validates it**: unknown or wrong-display keys are stored and ignored; a wrong binding renders blank or falls back to a table. Feedback is read-back, not errors.
Step 1 — display by data shape
- **Single number** → `scalar`; `smartscalar` for change vs previous period (needs a time breakout); `gauge`/`progress` for value vs target.
- **Measure across categories** → `bar`; `row` (horizontal) for long or many labels.
- **Trend** → `line`; `area` for stacked composition over time; `combo` for two related measures on unlike scales (never unrelated ones). Never `scalar`/`pie` for a trend.
- **Part-to-whole** → `pie`, ≤5 slices only; beyond that a sorted `bar`/`row`.
- **Distribution** → `bar` histogram (bin the field — `learn("query-dialect")`, binning).
- **Correlation** → `scatter` (third measure → bubble size).
- **Contributions to a total** → `waterfall`; **stage drop-off** → `funnel`; **flow between nodes** → `sankey`.
- **Geographic** → `map` (region/choropleth, or pin/grid from lat+long).
- **Precise values, many columns, nothing fits** → `table`; `pivot` for a two-dimension cross-tab.
Step 2 — bind output columns
**Every binding key takes output column-name strings, never field ids** — the names the query produces: a `count` aggregation is `count`, a breakout its field's name, a named aggregation its `name` option, a second `sum` in a stage `sum_2`. Empty `{}` is valid for a simple aggregate (auto-bound); set keys to pin or override:
{"display": "bar",
"visualization_settings": {"graph.dimensions": ["CATEGORY"], "graph.metrics": ["count"]}}- `bar`/`line`/`area`/`combo`/`row`/`scatter`: `graph.dimensions` (x; a 2nd entry = series breakout), `graph.metrics` (y). Stacked: `"stackable.stack_type": "stacked"` (`"normalized"` = 100%).
- `pie`: `{"pie.dimension": "CATEGORY", "pie.metric": "count"}`
- `scalar`: `{"scalar.field": "count"}` (only with >1 column)
- `funnel`: `{"funnel.dimension": "STAGE", "funnel.metric": "count"}`
- `map` region: `{"map.type": "region", "map.region": "us_states", "map.dimension": "STATE", "map.metric": "count"}`. `map.region`: `"us_states"` (dimension values = 2-letter state codes or state names, `"CA"`/`"California"`), `"world_countries"` (2-letter ISO codes or country names, `"US"`/`"United States"`), or an admin-added custom-GeoJSON key. Pin/grid: `"map.type": "pin"` + `map.latitude_column` + `map.longitude_column`.
- `sankey`: `{"sankey.source": "FROM", "sankey.target": "TO", "sankey.value": "count"}`
- `waterfall`: exactly one dimension + one metric.
- `table`: always renders; `table.columns` (`[{"name": …, "enabled": …}]`) orders and hides columns.
`column_settings`: JSON-string keys
Per-column formatting keys are **JSON-encoded arrays passed as strings**, inner quotes escaped — never an object key:
"visualization_settings": {
"column_settings": {
"[\"name\",\"TOTAL\"]": {"number_style": "currency", "currency": "USD", "decimals": 2},
"[\"name\",\"CREATED_AT\"]": {"date_style": "MMMM D, YYYY"}}}Always author `["name", "<output column>"]`. A `["ref", ["field", id, opts]]` key appears in read-back — don't author it; when editing such a card keep those keys verbatim.
Escape hatch and catalog
For anything intricate — combo charts, conditional formatting, pivot splits, click behavior — read a UI-built card with the look you want (`get_content {"items": [{"type": "question", "id": <id>}], "include": ["visualization_settings"]}`) and reuse its `visualization_settings` verbatim; the server produced it, so it's valid for that display. Every key with values and defaults, `series_settings`, conditional formatting, pivot splits, dashcard click behavior: `learn("visualization-settings", "settings")`.
Don't
- Don't invent display values — accepted, renders nothing.
- Don't put field ids in `graph.dimensions` / `pie.metric` / `scalar.field` — output column names only, or the chart renders blank.
- Don't write a `column_settings` key as an object, or author the `["ref", …]` form — ignored.
- Don't pick `pie` for >5 slices, `combo` for unrelated metrics, or `scalar`/`pie` for a trend — renders, misleads.
- Don't report a chart as rendering from the write response — its `visualization_settings` echo is what got stored, not proof anything draws.
To confirm
`get_content {"items": [{"type": "question", "id": <id>}], "include": ["visualization_settings"]}` returns `visualization_settings` as stored (alongside `display`, which every question read carries) — proof the settings saved, not that the chart renders. `question_write` also echoes `visualization_settings` in its response, so a write you just made needs no second call. `fields: ["visualization_settings"]` reads the blob alone; a card with nothing stored returns `{}`. Rendering (a region whose values don't match, a binding to a missing column) has no API check: call it unverified unless the user has viewed th
Read more
name: visualization-settings description: Choosing a card's `display` and authoring `visualization_settings` for question_write and patch_dashcard — which chart fits which data shape, the output-column-name rule, minimum keys per chart family, map regions, the column_settings JSON-string-key footgun. Triggers — "make this a bar/line/pie chart", "what chart should I use", "format as currency", "the card renders as a table instead of a chart", "conditional formatting", "region map".
Visualization: pick the chart, then set it
Two fields, set through `question_write` and mergeable per dashcard via `dashboard_write`'s `patch_dashcard`:
- **`display`** — `table`, `bar`, `line`, `area`, `row`, `pie`, `scalar`, `smartscalar`, `combo`, `pivot`, `funnel`, `map`, `scatter`, `waterfall`, `progress`, `gauge`, `sankey`. Nothing else: an unknown value (`bargraph`, `histogram`, `number`) is accepted and renders nothing; `scalar` **is** the Number viz.
- **`visualization_settings`** — a map keyed per display (`graph.*`, `pie.*`, `table.*`, …). **Nothing validates it**: unknown or wrong-display keys are stored and ignored; a wrong binding renders blank or falls back to a table. Feedback is read-back, not errors.
Step 1 — display by data shape
- **Single number** → `scalar`; `smartscalar` for change vs previous period (needs a time breakout); `gauge`/`progress` for value vs target.
- **Measure across categories** → `bar`; `row` (horizontal) for long or many labels.
- **Trend** → `line`; `area` for stacked composition over time; `combo` for two related measures on unlike scales (never unrelated ones). Never `scalar`/`pie` for a trend.
- **Part-to-whole** → `pie`, ≤5 slices only; beyond that a sorted `bar`/`row`.
- **Distribution** → `bar` histogram (bin the field — `learn("query-dialect")`, binning).
- **Correlation** → `scatter` (third measure → bubble size).
- **Contributions to a total** → `waterfall`; **stage drop-off** → `funnel`; **flow between nodes** → `sankey`.
- **Geographic** → `map` (region/choropleth, or pin/grid from lat+long).
- **Precise values, many columns, nothing fits** → `table`; `pivot` for a two-dimension cross-tab.
Step 2 — bind output columns
**Every binding key takes output column-name strings, never field ids** — the names the query produces: a `count` aggregation is `count`, a breakout its field's name, a named aggregation its `name` option, a second `sum` in a stage `sum_2`. Empty `{}` is valid for a simple aggregate (auto-bound); set keys to pin or override:
{"display": "bar",
"visualization_settings": {"graph.dimensions": ["CATEGORY"], "graph.metrics": ["count"]}}- `bar`/`line`/`area`/`combo`/`row`/`scatter`: `graph.dimensions` (x; a 2nd entry = series breakout), `graph.metrics` (y). Stacked: `"stackable.stack_type": "stacked"` (`"normalized"` = 100%).
- `pie`: `{"pie.dimension": "CATEGORY", "pie.metric": "count"}`
- `scalar`: `{"scalar.field": "count"}` (only with >1 column)
- `funnel`: `{"funnel.dimension": "STAGE", "funnel.metric": "count"}`
- `map` region: `{"map.type": "region", "map.region": "us_states", "map.dimension": "STATE", "map.metric": "count"}`. `map.region`: `"us_states"` (dimension values = 2-letter state codes or state names, `"CA"`/`"California"`), `"world_countries"` (2-letter ISO codes or country names, `"US"`/`"United States"`), or an admin-added custom-GeoJSON key. Pin/grid: `"map.type": "pin"` + `map.latitude_column` + `map.longitude_column`.
- `sankey`: `{"sankey.source": "FROM", "sankey.target": "TO", "sankey.value": "count"}`
- `waterfall`: exactly one dimension + one metric.
- `table`: always renders; `table.columns` (`[{"name": …, "enabled": …}]`) orders and hides columns.
`column_settings`: JSON-string keys
Per-column formatting keys are **JSON-encoded arrays passed as strings**, inner quotes escaped — never an object key:
"visualization_settings": {
"column_settings": {
"[\"name\",\"TOTAL\"]": {"number_style": "currency", "currency": "USD", "decimals": 2},
"[\"name\",\"CREATED_AT\"]": {"date_style": "MMMM D, YYYY"}}}Always author `["name", "<output column>"]`. A `["ref", ["field", id, opts]]` key appears in read-back — don't author it; when editing such a card keep those keys verbatim.
Escape hatch and catalog
For anything intricate — combo charts, conditional formatting, pivot splits, click behavior — read a UI-built card with the look you want (`get_content {"items": [{"type": "question", "id": <id>}], "include": ["visualization_settings"]}`) and reuse its `visualization_settings` verbatim; the server produced it, so it's valid for that display. Every key with values and defaults, `series_settings`, conditional formatting, pivot splits, dashcard click behavior: `learn("visualization-settings", "settings")`.
Don't
- Don't invent display values — accepted, renders nothing.
- Don't put field ids in `graph.dimensions` / `pie.metric` / `scalar.field` — output column names only, or the chart renders blank.
- Don't write a `column_settings` key as an object, or author the `["ref", …]` form — ignored.
- Don't pick `pie` for >5 slices, `combo` for unrelated metrics, or `scalar`/`pie` for a trend — renders, misleads.
- Don't report a chart as rendering from the write response — its `visualization_settings` echo is what got stored, not proof anything draws.
To confirm
`get_content {"items": [{"type": "question", "id": <id>}], "include": ["visualization_settings"]}` returns `visualization_settings` as stored (alongside `display`, which every question read carries) — proof the settings saved, not that the chart renders. `question_write` also echoes `visualization_settings` in its response, so a write you just made needs no second call. `fields: ["visualization_settings"]` reads the blob alone; a card with nothing stored returns `{}`. Rendering (a region whose values don't match, a binding to a missing column) has no API check: call it unverified unless the user has viewed th
Metabase is the easy, open-source way for everyone in your company to ask questions and learn from data.
Other skills on metabase.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about visualization-settings.
- Which coding agents can use visualization-settings?
- visualization-settings installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

