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 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;scalaris 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;smartscalarfor change vs previous period (needs a time breakout);gauge/progressfor value vs target. - Measure across categories →
bar;row(horizontal) for long or many labels. - Trend →
line;areafor stacked composition over time;combofor two related measures on unlike scales (never unrelated ones). Neverscalar/piefor a trend. - Part-to-whole →
pie, ≤5 slices only; beyond that a sortedbar/row. - Distribution →
barhistogram (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;pivotfor 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"}mapregion:{"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_settingskey as an object, or author the["ref", …]form — ignored. - Don't pick
piefor >5 slices,combofor unrelated metrics, orscalar/piefor a trend — renders, misleads. - Don't report a chart as rendering from the write response — its
visualization_settingsecho 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 the card.
Related skills
Explore other skills from the same source repository.
--- name: documents description: The Metabase-flavored Markdown grammar for document_write — the CommonMark subset, {% card %} embeds, {% entity %} links, ::: layout containers (flex / supporting / resize) and nesting rules, surgical and their limits, card clones, comments....