@metabase/cli 0.1.6 → 0.1.8-alpha.cleanup-workspaces.75fe1d5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +16 -238
  3. package/dist/add-collection-BGifL-7Y.mjs +10 -0
  4. package/dist/{add-collection-BU8r3r2M.mjs → add-collection-C5ijC4e-.mjs} +8 -6
  5. package/dist/{archive-BNinrUak.mjs → archive-2GZVq0n2.mjs} +7 -8
  6. package/dist/{archive-lWgqiFAt.mjs → archive-C5pXDf6a.mjs} +5 -6
  7. package/dist/{archive-C1enZgKV.mjs → archive-CTEybKPq.mjs} +6 -7
  8. package/dist/{archive-DMPS8Kih.mjs → archive-DEtOH_jf.mjs} +5 -6
  9. package/dist/{archive-CDA0KxL8.mjs → archive-DTrn90If.mjs} +5 -6
  10. package/dist/{archive-CRhiBpPJ.mjs → archive-nqKJOyOt.mjs} +5 -6
  11. package/dist/auth-Divr9vkY.mjs +19 -0
  12. package/dist/{body-DjdFxjpg.mjs → body-BMyDvOjf.mjs} +2 -2
  13. package/dist/{branches-B1WRfG7-.mjs → branches-B7Zf-nxr.mjs} +5 -6
  14. package/dist/{cancel-Dl_Ho056.mjs → cancel-WwjcCE2R.mjs} +6 -7
  15. package/dist/{cancel-task-CdigdCaO.mjs → cancel-task-E4cCeJTc.mjs} +6 -7
  16. package/dist/{render-CfznwleY.mjs → capabilities-Dj3NgPHg.mjs} +66 -10
  17. package/dist/card-BHAcovSB.mjs +20 -0
  18. package/dist/{card-DlCAaAPq.mjs → card-BtPiKjbB.mjs} +1 -1
  19. package/dist/{cards-BGiJS675.mjs → cards-BW0Spirk.mjs} +4 -5
  20. package/dist/cli.mjs +23 -24
  21. package/dist/collection-DZx-vWb2.mjs +20 -0
  22. package/dist/{create-BNiva__H.mjs → create-BV_VpyyI.mjs} +11 -12
  23. package/dist/{create-BTcpaop_.mjs → create-B_Ysb6Ex.mjs} +7 -8
  24. package/dist/{create-BYlIju0b.mjs → create-BgeD2GMZ.mjs} +10 -11
  25. package/dist/{create-DGth_uOp.mjs → create-Bxkik5Fs.mjs} +9 -10
  26. package/dist/{create-CzzrbL0u.mjs → create-CZfHpi_I.mjs} +8 -9
  27. package/dist/{create-Be_0Vier.mjs → create-CbwFOL9h.mjs} +8 -9
  28. package/dist/{create-dhxPxfF3.mjs → create-CbyY4xTG.mjs} +12 -13
  29. package/dist/{create-CwGtmwqm.mjs → create-HCCdsuZR.mjs} +9 -10
  30. package/dist/{create-branch-DKZkoQ64.mjs → create-branch-BNRq3tO2.mjs} +6 -7
  31. package/dist/{current-task-CCRzm0_7.mjs → current-task-ZvTjL4hP.mjs} +7 -8
  32. package/dist/dashboard-DCWGcx3e.mjs +21 -0
  33. package/dist/{database-lH-B3G1I.mjs → database-_Td7rG4o.mjs} +1 -1
  34. package/dist/db-bD6fwLwc.mjs +22 -0
  35. package/dist/{delete-ZjnV35OJ.mjs → delete-D5-ITH10.mjs} +8 -7
  36. package/dist/{delete-Dimc-2y8.mjs → delete-DvmHTMwf.mjs} +8 -7
  37. package/dist/{delete-runtime-B6RQo_pw.mjs → delete-runtime-Bo7v-0au.mjs} +6 -6
  38. package/dist/{delete-table-agZJpivt.mjs → delete-table-DjmO5Qe4.mjs} +8 -7
  39. package/dist/{dirty-D4d0yHqj.mjs → dirty-BJgU1SGQ.mjs} +5 -6
  40. package/dist/{eid-BXzaQh0o.mjs → eid-pqSVgO2g.mjs} +12 -8
  41. package/dist/{error-C9S6PN3-.mjs → error-Br6irjpM.mjs} +3 -2
  42. package/dist/{export-DTygoXBP.mjs → export-gVXTOxC2.mjs} +10 -10
  43. package/dist/field-BFWc8q2W.mjs +18 -0
  44. package/dist/{field-yomXlkvl.mjs → field-MGxpNQUH.mjs} +12 -3
  45. package/dist/{fields-CoQi99gv.mjs → fields-DK2TYRK2.mjs} +6 -7
  46. package/dist/{get-DQTZG_NP.mjs → get-BjArD6Er.mjs} +4 -5
  47. package/dist/{get-C3HdQ91a.mjs → get-C1RSXOhf.mjs} +5 -6
  48. package/dist/{get-DSWFjy7O.mjs → get-C3RI_hKG.mjs} +4 -5
  49. package/dist/{get-C_w1kvN3.mjs → get-C7UEEICF.mjs} +6 -7
  50. package/dist/{get-Bzys7vgp.mjs → get-D-3tOuWQ.mjs} +4 -5
  51. package/dist/{get-Hc93A0Yz.mjs → get-DDF7_apz.mjs} +5 -6
  52. package/dist/{get-D3SbEQSE.mjs → get-DYgLtHb8.mjs} +4 -5
  53. package/dist/{get-CP3Z3NiH.mjs → get-DnK3WGDH.mjs} +6 -7
  54. package/dist/{get-C2p383Qc.mjs → get-DnyZMx-0.mjs} +5 -6
  55. package/dist/{get-DFxZXaKz.mjs → get-DtG5ytvs.mjs} +5 -7
  56. package/dist/{get-CzuzeKSe.mjs → get-Dy4NedW5.mjs} +7 -8
  57. package/dist/{get-Ddr0XLh7.mjs → get-_JI4GiPa.mjs} +5 -6
  58. package/dist/{get-run-B7sKdaDU.mjs → get-run-CZwIo5QR.mjs} +5 -6
  59. package/dist/{get-lb7q3JYs.mjs → get-xo5_PD1n.mjs} +3 -4
  60. package/dist/git-sync-Ch00miSa.mjs +28 -0
  61. package/dist/{has-remote-changes-BY10-nnE.mjs → has-remote-changes-tuuoPdHV.mjs} +7 -7
  62. package/dist/{import-CiMz4Wz-.mjs → import-BOzyRPTk.mjs} +11 -10
  63. package/dist/{is-dirty-BZOaryxT.mjs → is-dirty-BXN1F9yR.mjs} +5 -5
  64. package/dist/is-dirty-Bc1UHnBU.mjs +9 -0
  65. package/dist/{items-BWfvkY-J.mjs → items-CC4sSWv7.mjs} +4 -5
  66. package/dist/{list-SOG0whQ-.mjs → list-BUpOvQsd.mjs} +3 -4
  67. package/dist/{list-Clz5igWg.mjs → list-BlMpI88r.mjs} +4 -6
  68. package/dist/{list-2j7GsXsl.mjs → list-BqTdYtOW.mjs} +3 -4
  69. package/dist/{list-C3hfovHv.mjs → list-BvTuDJZp.mjs} +4 -5
  70. package/dist/{list-DZ8fNUoQ.mjs → list-C48ZZLM5.mjs} +6 -7
  71. package/dist/{list-BI4zr8LW.mjs → list-CJe45jFT.mjs} +6 -7
  72. package/dist/{list-D4sFiqX8.mjs → list-CMoG5_mh.mjs} +5 -6
  73. package/dist/{list-d58BprgJ.mjs → list-CkI3hESA.mjs} +4 -5
  74. package/dist/{list-sD5N3fGk.mjs → list-CybLzPZw.mjs} +6 -7
  75. package/dist/{list-CL7eCOQE.mjs → list-DLhU81LP.mjs} +3 -4
  76. package/dist/{list-Brgh-Z2v.mjs → list-Ddyb5T-7.mjs} +4 -5
  77. package/dist/{list-DXH7TlkU.mjs → list-tveV3LKh.mjs} +4 -5
  78. package/dist/{list--OYdUTtu.mjs → list-u7cmunGG.mjs} +4 -5
  79. package/dist/{login-Bm2AnCez.mjs → login-Bfsf6jLq.mjs} +17 -14
  80. package/dist/{logout-BlyRJODO.mjs → logout-BCsE_1Yg.mjs} +9 -9
  81. package/dist/{manifest-BBR46KFM.mjs → manifest-B_OfG-Ss.mjs} +1 -2
  82. package/dist/measure-D_eiCKq9.mjs +19 -0
  83. package/dist/{metadata-B8ZSF9LA.mjs → metadata-Cbqx1-yW.mjs} +7 -8
  84. package/dist/{metadata-DqiI2q9q.mjs → metadata-CzbGy4b-.mjs} +6 -7
  85. package/dist/{parse-id-lk_K-CEF.mjs → parse-id-DBwNYb4Y.mjs} +1 -1
  86. package/dist/{path-AEtZ3mBq.mjs → path-DJ-DfUGC.mjs} +4 -6
  87. package/dist/{poll-DHKDpCiq.mjs → poll-DWkVceIk.mjs} +1 -1
  88. package/dist/{poll-task-Cooi0lQV.mjs → poll-task-DOvUDrgL.mjs} +19 -3
  89. package/dist/{preflight-aXV5LyDs.mjs → preflight-D-l6sLCX.mjs} +3 -3
  90. package/dist/{query-AaKzYnTY.mjs → query--05TZxmk.mjs} +9 -8
  91. package/dist/{query-BlsVNZpD.mjs → query-BbbDpt3u.mjs} +12 -12
  92. package/dist/query-result-PoQE05Pe.mjs +19 -0
  93. package/dist/{remove-collection-CoCmrrQs.mjs → remove-collection-Dco3yXa-.mjs} +10 -9
  94. package/dist/{render-OQn3iRsI.mjs → render-BQHJhSP1.mjs} +1 -1
  95. package/dist/{rescan-values-C0FDsjT7.mjs → rescan-values-B1vE6I62.mjs} +9 -10
  96. package/dist/{run-B4Wn43zm.mjs → run-CWb_yZbK.mjs} +14 -14
  97. package/dist/{runs-Bbaszr18.mjs → runs-BLwEO5sW.mjs} +5 -6
  98. package/dist/{runtime-Dmv5VtUK.mjs → runtime-DKLBPRhd.mjs} +16 -87
  99. package/dist/{schema-tables-CaWinbuK.mjs → schema-tables-D1qE25GS.mjs} +6 -7
  100. package/dist/{schemas-DUgGpAyB.mjs → schemas-CFuYnQDg.mjs} +4 -5
  101. package/dist/{search-BLrBXLUk.mjs → search-CF4A051Z.mjs} +4 -5
  102. package/dist/segment-BP_vDpYM.mjs +19 -0
  103. package/dist/{set-DfGsta5O.mjs → set-BJ8Rs-vI.mjs} +7 -7
  104. package/dist/setting-EYI0HqTq.mjs +17 -0
  105. package/dist/{setup-C9ikBRw_.mjs → setup-DI6EGOdH.mjs} +7 -8
  106. package/dist/{skills-CiN1OQ8W.mjs → skills-BDPVoBWU.mjs} +33 -2
  107. package/dist/{skills-CUHIcQS6.mjs → skills-CRECMGAo.mjs} +3 -3
  108. package/dist/snippet-BwK58D1F.mjs +19 -0
  109. package/dist/{stash-EIDcSvpF.mjs → stash-BpPS0uG8.mjs} +11 -10
  110. package/dist/{status-95ElRAu9.mjs → status-BqFA7Tqr.mjs} +10 -8
  111. package/dist/{status-B0_MiZEf.mjs → status-CU5LYX0i.mjs} +4 -5
  112. package/dist/{summary-C12LiEuJ.mjs → summary-CraRyIM_.mjs} +5 -6
  113. package/dist/{sync-schema-Ba8M3DiX.mjs → sync-schema-DkfwPESH.mjs} +9 -10
  114. package/dist/table-CJMN6ql8.mjs +19 -0
  115. package/dist/{table-C7a5V6Zn.mjs → table-CkWja2UH.mjs} +1 -1
  116. package/dist/transform-BqV5ssDN.mjs +24 -0
  117. package/dist/transform-job-DvdOLV9b.mjs +19 -0
  118. package/dist/{tree-Des2ZG9d.mjs → tree-Bd1i-Y4U.mjs} +3 -4
  119. package/dist/{update-DzAN4SPj.mjs → update-BLZHmA0X.mjs} +10 -11
  120. package/dist/{update-CyIZdbIQ.mjs → update-BncGqeWt.mjs} +9 -10
  121. package/dist/{update-DSgceARZ.mjs → update-C4f-8fsU.mjs} +9 -10
  122. package/dist/{update-njHe3j-s.mjs → update-CgpgyDjd.mjs} +10 -11
  123. package/dist/{update-DBi5U8zb.mjs → update-CsYa--Eb.mjs} +12 -13
  124. package/dist/{update-mYVnoYNV.mjs → update-DA9aPhRr.mjs} +11 -12
  125. package/dist/{update-_QfgNa53.mjs → update-DtO-1_tj.mjs} +10 -11
  126. package/dist/{update-Bx54nWEI.mjs → update-IPsv9Pj0.mjs} +12 -13
  127. package/dist/{update-F6DmZncY.mjs → update-OZoOTnlY.mjs} +9 -10
  128. package/dist/{update-dashcard-wpSjv4M7.mjs → update-dashcard-D0VAwfNO.mjs} +8 -9
  129. package/dist/{upgrade-iAuvhX-W.mjs → upgrade-Bb_q7hJ7.mjs} +41 -28
  130. package/dist/{uuid-CMKnS8-z.mjs → uuid-DTdRruiz.mjs} +3 -4
  131. package/dist/{validate-dPEOnOf8.mjs → validate-CqVB_3_p.mjs} +1 -1
  132. package/dist/{validate-query-Cw6WE5Y8.mjs → validate-query-BUTNK00f.mjs} +2 -2
  133. package/dist/values-Bpt6obVW.mjs +44 -0
  134. package/dist/{verify-D5YtTqqp.mjs → verify-COt2q73-.mjs} +1 -1
  135. package/dist/{wait-Bv3Tsnv4.mjs → wait-DoKPlspq.mjs} +8 -9
  136. package/dist/{wait-flags-Dzq9BGQY.mjs → wait-flags-D1jNm0JK.mjs} +2 -2
  137. package/package.json +1 -1
  138. package/skill-data/core/SKILL.md +15 -28
  139. package/skill-data/git-sync/SKILL.md +8 -63
  140. package/skill-data/mbql/SKILL.md +2 -0
  141. package/skill-data/transform/SKILL.md +16 -6
  142. package/skill-data/visualization/SKILL.md +158 -0
  143. package/skill-data/visualization/references/settings.md +414 -0
  144. package/skills/metabase-cli/SKILL.md +1 -1
  145. package/dist/add-collection-C0w6ACQF.mjs +0 -11
  146. package/dist/auth-CzXb_zB2.mjs +0 -19
  147. package/dist/capabilities-7e9MgquN.mjs +0 -29
  148. package/dist/card-DP4rfoOi.mjs +0 -21
  149. package/dist/collection-tY18ezvn.mjs +0 -21
  150. package/dist/create-CHF313Qg.mjs +0 -52
  151. package/dist/credentials-dzeq7ckm.mjs +0 -88
  152. package/dist/dashboard-ChM_Tu0l.mjs +0 -22
  153. package/dist/database-CIXwHKjK.mjs +0 -17
  154. package/dist/db-DrQn_i3W.mjs +0 -22
  155. package/dist/delete-CM3jnAeQ.mjs +0 -100
  156. package/dist/deprovision-CwxcIT3k.mjs +0 -65
  157. package/dist/docker-Oq80q3tu.mjs +0 -515
  158. package/dist/field-Z6Pcxf4n.mjs +0 -19
  159. package/dist/git-sync-CiGAad76.mjs +0 -28
  160. package/dist/is-dirty-Ume4oV0j.mjs +0 -10
  161. package/dist/license-Dxarh-gG.mjs +0 -17
  162. package/dist/list-zSO0DMw-.mjs +0 -36
  163. package/dist/logs-CywPikkL.mjs +0 -60
  164. package/dist/measure-C44EK_xt.mjs +0 -20
  165. package/dist/parse-schemas-BqUdWUwq.mjs +0 -12
  166. package/dist/process-C7V8LJ-j.mjs +0 -105
  167. package/dist/provision-UWcNDoDe.mjs +0 -82
  168. package/dist/ps-CJU0EbrC.mjs +0 -80
  169. package/dist/ps-DEroLgbI.mjs +0 -11
  170. package/dist/remove-BFWun0e8.mjs +0 -63
  171. package/dist/segment-B3Uwwcsm.mjs +0 -20
  172. package/dist/set-B8cUbRLD.mjs +0 -68
  173. package/dist/setting-D2p2MA7f.mjs +0 -18
  174. package/dist/snippet-B7D0uWlz.mjs +0 -20
  175. package/dist/start-3PX3ahjT.mjs +0 -413
  176. package/dist/status-CEplmC44.mjs +0 -34
  177. package/dist/stop-CQ0XGrN8.mjs +0 -83
  178. package/dist/table-e6h8SLVX.mjs +0 -20
  179. package/dist/transform-BMYh1lsC.mjs +0 -25
  180. package/dist/transform-job-Cm7z5TfH.mjs +0 -20
  181. package/dist/update-DHZubok3.mjs +0 -77
  182. package/dist/url-DWaT6WIZ.mjs +0 -56
  183. package/dist/values-BfSTAbzc.mjs +0 -37
  184. package/dist/wait-8yV9_WIo.mjs +0 -19
  185. package/dist/workspace-BBXJczJK.mjs +0 -72
  186. package/dist/workspace-CKLZrR7l.mjs +0 -26
  187. package/dist/workspace-credentials-BXpABsNZ.mjs +0 -100
  188. package/dist/yaml-YTQiYJ9s.mjs +0 -43
  189. package/skill-data/viz/SKILL.md +0 -137
  190. package/skill-data/viz/references/settings.md +0 -312
  191. package/skill-data/workspace/SKILL.md +0 -390
  192. /package/dist/{body-flags-D7q87Btw.mjs → body-flags-D78h_-Ua.mjs} +0 -0
  193. /package/dist/{input-cMSEqISy.mjs → input-xewHccej.mjs} +0 -0
  194. /package/dist/{parse-enum-CrEWOhuY.mjs → parse-enum-Dr7ACrTR.mjs} +0 -0
  195. /package/dist/{prompt-CFKoys7k.mjs → prompt-u4WhE4T5.mjs} +0 -0
  196. /package/dist/{snippet-COggaWxx.mjs → snippet-xh42tYly.mjs} +0 -0
  197. /package/dist/{transform-GTW3G-01.mjs → transform-DyJb0bV0.mjs} +0 -0
  198. /package/dist/{transform-job-DeTDPMxt.mjs → transform-job-DpDGoqQt.mjs} +0 -0
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: visualization
3
+ description: Pick the right `display` (chart type) for a card's data and author its `visualization_settings` via the `mb` CLI. Covers which chart fits which data shape, the required and optional settings per chart type, the rule that settings reference OUTPUT columns by name, minimum-viable settings per chart family, and the `column_settings` JSON-string-key footgun. The full per-chart key catalog lives in references. Load whenever choosing or shaping how a card renders — "what chart should I use for this", "create a bar chart", "make this a line chart", "turn this into a pie", "map this by state", "format this column as currency", "set the pie dimension and metric", "the card renders as a table instead of a chart", "add conditional formatting", or any `display` / `visualization_settings` work.
4
+ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
5
+ ---
6
+
7
+ # Visualization: pick the chart, then set it
8
+
9
+ A card has two presentation fields alongside its `dataset_query`:
10
+
11
+ - **`display`** — the chart type (`bar`, `line`, `pie`, `scalar`, `map`, `table`, …). One closed set; pick from the enum below.
12
+ - **`visualization_settings`** — a map whose keys are **namespaced by `display`** (`graph.*` for bar/line/area/combo, `pie.*` for pie, `table.*` for table, …). The server stores almost anything and **silently ignores keys that don't apply** to the chosen `display`.
13
+
14
+ Nothing validates `visualization_settings` — there is no pre-flight to fail past. A `display` typo or a misnamed key is accepted by the API; the card just renders as a default table or drops the setting. So **the feedback loop is read-back, not pre-flight**: after `card create`/`update`, confirm with `mb card get <id> --full --json` (or open the card) that it rendered as intended.
15
+
16
+ General flag conventions and body-input precedence live in the `core` skill (`mb skills get core`); the `dataset_query` itself is the `mbql` skill's job (`mb skills get mbql`). This skill is only about how the result is displayed.
17
+
18
+ Two steps: **(1) pick the `display` that fits the data**, then **(2) bind the data columns and set options**.
19
+
20
+ ## Step 1 — pick the `display` for your data
21
+
22
+ Decide which relationship in the data matters most, then pick the chart. The shape each one needs is in the per-display table further down.
23
+
24
+ - **Single headline number** → `scalar` (one KPI). `smartscalar` when the story is the change vs the previous period. `gauge`/`progress` for one value against a target/goal.
25
+ - **Compare a measure across categories** → `bar` (vertical). Use `row` (horizontal bar) when labels are long or there are many categories. Sort by value unless the dimension has a natural order.
26
+ - **Change over time / trend** → `line` for a continuous series; `bar`/`area` for a few discrete periods. Two measures on unlike scales → `combo` (line + bar, dual-axis) — only when the metrics are genuinely related.
27
+ - **Part-to-whole, one snapshot** → `pie`, but only for a meaningful whole with **≤5 slices**; beyond that use a sorted `bar`/`row`. Composition over time → stacked `area`/`bar`.
28
+ - **Distribution / spread / outliers** → `boxplot` (especially comparing several groups).
29
+ - **Correlation between two measures** → `scatter` (a third measure → bubble size).
30
+ - **Sequential additive contributions** (start → +/− steps → total) → `waterfall`.
31
+ - **Stage drop-off in an ordered, cumulative funnel** → `funnel`.
32
+ - **Flow volume between nodes** (source → target + weight) → `sankey`.
33
+ - **Geographic** → `map`: region/choropleth (a region dimension + a measure), pin (lat + long), or grid/heat (coordinates + measure).
34
+ - **Precise values, many columns, mixed types, or no chart fits** → `table`; `pivot` for a cross-tab of two dimensions; `object` for a single record's detail.
35
+
36
+ Closed `display` enum (card-level, non-hidden): `table`, `bar`, `line`, `area`, `row`, `pie`, `scalar`, `smartscalar`, `combo`, `pivot`, `funnel`, `map`, `scatter`, `waterfall`, `progress`, `gauge`, `object`, `sankey`, `boxplot`. (`scalar` **is** the "Number" viz — `display: number` is a legacy serialization alias, not a registered visualization; use `scalar`. `list` exists but is hidden — don't pick it. `heading`/`text`/`link`/`iframe`/`action` are dashcard virtuals, not standalone cards — see references.) An unknown `display` is accepted by the API but renders nothing — typos like `bargraph`/`linechart` are the most common "why is my chart blank" cause.
37
+
38
+ ## Step 2 — bind data columns and set options
39
+
40
+ ### Which chart for which data, and what to set
41
+
42
+ **Use for** is the data shape each chart suits. **Required** is the minimum to set for it to render as a chart — omit it and the card falls back to a "which columns?" prompt. Everything else is optional (full keys in references). **Empty `"visualization_settings": {}` is valid**: for a simple aggregate the binding is auto-picked, so set keys only to pin or override.
43
+
44
+ | `display` | Use for | Required |
45
+ | --------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
46
+ | `scalar` | 1 row, 1 column | — (`scalar.field` only if >1 column) |
47
+ | `smartscalar` | one value grouped by a single **time** field | — (needs a time breakout; `scalar.field` auto) |
48
+ | `gauge` | 1 row, 1 numeric column | — (`gauge.segments` auto) |
49
+ | `progress` | 1 row, ≥1 numeric column | — (`progress.goal` defaults to 0) |
50
+ | `bar` `line` `area` `combo` | >1 row, ≥2 cols, ≥1 dimension + ≥1 measure | `graph.dimensions`, `graph.metrics` |
51
+ | `row` | as bar; prefer for long/many category labels | `graph.dimensions`, `graph.metrics` |
52
+ | `scatter` | two numeric measures (correlation) | `graph.dimensions`, `graph.metrics` (`scatter.bubble` opt) |
53
+ | `waterfall` | exactly 1 dimension + ≥1 measure; sequential | `graph.dimensions` (1), `graph.metrics` (1) |
54
+ | `boxplot` | ≥3 cols, ≥2 dimensions, ≥1 measure | `graph.dimensions`, `graph.metrics` |
55
+ | `pie` | ≥2 rows, ≥2 cols, ≥1 dimension + ≥1 measure; ≤~5 slices | `pie.dimension`, `pie.metric` |
56
+ | `funnel` | 2 columns (stage + value); ordered stages | `funnel.dimension`, `funnel.metric` |
57
+ | `map` (region) | a string/region dimension + a measure | `map.region`, `map.dimension`, `map.metric` |
58
+ | `map` (pin/grid) | latitude + longitude columns | `map.latitude_column`, `map.longitude_column` |
59
+ | `sankey` | ≥3 cols, ≥2 non-date dimensions, ≥1 measure; acyclic flow | `sankey.source`, `sankey.target`, `sankey.value` |
60
+ | `pivot` | ≥2 cols, all aggregated/breakout | — (`pivot_table.column_split` auto) |
61
+ | `table` `object` | anything (table is the universal fallback) | — (always renders) |
62
+
63
+ ### The rule that trips everyone: settings name **output columns**, by name
64
+
65
+ `graph.dimensions`, `graph.metrics`, `pie.dimension`, `pie.metric`, `scalar.field`, `funnel.metric`, `map.latitude_column`, `sankey.source`, … all take **output column-name strings** — the names the query _produces_, not field ids. A `count` aggregation outputs the column `count`; a breakout on a field outputs that field's name; a named aggregation outputs its `name`. These strings are **identical in the API form and the portable (git-sync) form** — no numeric-vs-name footgun here.
66
+
67
+ So the names you put in `visualization_settings` come from the query's output, not from `mb field`/`mb table`. If you set `name` on an aggregation (see the `mbql` skill), use that same string here.
68
+
69
+ ## Minimum-viable settings per chart family (API form)
70
+
71
+ Each block is the `visualization_settings` to pair with the given `display`. The `dataset_query` is elided — build it per the `mbql` skill. Output columns (`CATEGORY`, `count`, …) are whatever the query's breakout/aggregation produce.
72
+
73
+ **Bar / line / area / combo** — one dimension on the x-axis, one or more metrics (the four share an identical key set; switch `display` freely):
74
+
75
+ ```json
76
+ "display": "bar",
77
+ "visualization_settings": { "graph.dimensions": ["CATEGORY"], "graph.metrics": ["count"] }
78
+ ```
79
+
80
+ (Multiple metrics: `"graph.metrics": ["count","sum"]`. Stacked: add `"stackable.stack_type": "stacked"` — or `"normalized"` for 100%. A second dimension in `graph.dimensions` becomes a series breakout.)
81
+
82
+ **Row** — same keys; axes are visually swapped (horizontal bars).
83
+
84
+ **Pie** — one dimension, one metric:
85
+
86
+ ```json
87
+ "display": "pie",
88
+ "visualization_settings": { "pie.dimension": "CATEGORY", "pie.metric": "count" }
89
+ ```
90
+
91
+ **Scalar** (single big number) — the field to surface (only needed if >1 column):
92
+
93
+ ```json
94
+ "display": "scalar",
95
+ "visualization_settings": { "scalar.field": "count" }
96
+ ```
97
+
98
+ **Map (region/choropleth)** — region map + dimension + metric:
99
+
100
+ ```json
101
+ "display": "map",
102
+ "visualization_settings": { "map.type": "region", "map.region": "us_states", "map.dimension": "STATE", "map.metric": "count" }
103
+ ```
104
+
105
+ **Table** — column order/visibility plus per-column formatting:
106
+
107
+ ```json
108
+ "display": "table",
109
+ "visualization_settings": {
110
+ "table.columns": [ { "name": "CATEGORY", "enabled": true }, { "name": "count", "enabled": true } ],
111
+ "column_settings": { "[\"name\",\"count\"]": { "column_title": "Orders" } }
112
+ }
113
+ ```
114
+
115
+ ## `column_settings`: the JSON-string-key footgun
116
+
117
+ `column_settings` is a map **whose keys are themselves JSON-encoded arrays** — so inside a JSON body the inner quotes must be escaped. The key is a _string_, never an object.
118
+
119
+ - **Prefer the name form:** `["name", "<output column name>"]` → in a JSON body, `"[\"name\",\"count\"]"`. This is the canonical key Metabase writes, and it's **identical in API and portable form**. Use it unless you have a reason not to.
120
+ - **Ref form (legacy order!):** `["ref", ["field", <id>, <opts>]]`. The inner field ref uses the **legacy MBQL-4 order** `["field", id, options]` (id **second**) — _not_ the MBQL-5 order you use in `dataset_query`. In the API form `<id>` is the numeric field id. Because the order differs, this form is easy to get wrong — reach for the name form instead.
121
+
122
+ ```json
123
+ "column_settings": {
124
+ "[\"name\",\"TOTAL\"]": { "number_style": "currency", "currency": "USD", "decimals": 2 },
125
+ "[\"name\",\"CREATED_AT\"]": { "date_style": "MMMM D, YYYY" }
126
+ }
127
+ ```
128
+
129
+ The exhaustive per-column key list (number/date formatting, `view_as`, alignment, mini bars, click behavior) is in the references file.
130
+
131
+ ## Escape hatch: pull a real card instead of authoring from scratch
132
+
133
+ For anything beyond a single dimension + metric — combo charts, conditional formatting, pivot splits, click behavior, series colors — the cheapest **correct** path is to build it once in the Metabase UI and copy the result:
134
+
135
+ ```bash
136
+ mb card get <id> --full --json | jq '.visualization_settings'
137
+ ```
138
+
139
+ Paste that block into your `card create`/`update` body. The server produced it, so it's valid for that `display`. This beats guessing keys from memory, and it's token-cheap.
140
+
141
+ ## Full per-visualization key catalog
142
+
143
+ The body above covers the high-frequency 90%. The complete per-chart key tables — every key with its values and defaults, the data shape each chart suits, the full `column_settings` and `series_settings` vocabularies, conditional formatting, pivot splits, virtual cards (heading/text/link/iframe), and click behavior — live in the references file. Load on demand, not by default:
144
+
145
+ ```bash
146
+ mb skills get visualization --full # appends references/settings.md to this body
147
+ mb skills path visualization # → the skill dir; then Read references/settings.md
148
+ ```
149
+
150
+ ## Don't
151
+
152
+ - Don't invent `display` values (`bargraph`, `linechart`, `histogram`) or use `number`/`list` — use the closed non-hidden enum; the API accepts a typo and renders nothing.
153
+ - Don't put numeric field ids in `graph.dimensions`/`pie.metric`/`scalar.field`/`map.latitude_column` etc. — they take **output column-name strings**.
154
+ - Don't reach for a `pie` with >5 slices, a `combo` of unrelated metrics, or a `pie`/`scalar` to show a trend — see Step 1.
155
+ - Don't write a `column_settings` key as an object — it's a JSON **string** (`"[\"name\",\"COL\"]"`), inner quotes escaped.
156
+ - Don't use the MBQL-5 field-ref order inside a `column_settings` `["ref", …]` key — that key uses the **legacy** `["field", id, opts]` order. Prefer the `["name", …]` form.
157
+ - Don't expect a pre-flight to catch viz mistakes — there is none. Verify by reading the card back.
158
+ - Don't hand-author complex charts when you can pull a working `visualization_settings` from a UI-built card.
@@ -0,0 +1,414 @@
1
+ # visualization_settings — per-chart key reference
2
+
3
+ Authorable keys per `display`, plus the data shape each chart suits and the minimum needed to render. Set keys only to override defaults — an empty `{}` works for a simple aggregate.
4
+
5
+ All column-naming keys (`graph.dimensions`, `pie.dimension`, `table.columns[].name`, `map.latitude_column`, …) take **output column-name strings** — the names the query produces. Every key and value below is identical in the API form (`mb card create`) and the portable git-sync form, with two exceptions: `column_settings` `["ref", …]` keys and click-behavior dimension targets carry a numeric field id in the API form and a name-path in the portable form. In a JSON body, `column_settings` keys are escaped strings: `"[\"name\",\"TOTAL\"]"`.
6
+
7
+ ---
8
+
9
+ # Cartesian — `bar`, `line`, `area`, `combo`, `scatter`, `waterfall`, `row`, `boxplot`
10
+
11
+ `bar`/`line`/`area`/`combo` share an identical key set (combo just defaults the first series to a line, the rest to bars). `scatter`/`waterfall`/`row`/`boxplot` use a subset plus their own extras. Most allow up to 2 dimensions and unlimited metrics; **waterfall is 1 dimension + 1 metric**, boxplot allows 2 dimensions.
12
+
13
+ ## Shared keys
14
+
15
+ **Data binding**
16
+
17
+ | Key | Type | Notes |
18
+ | -------------------- | -------------------- | ----------------------------------------------------------------------------------- |
19
+ | `graph.dimensions` | string[] (col names) | X-axis. Index 0 = x-axis; a 2nd entry = series breakout. |
20
+ | `graph.metrics` | string[] (col names) | Y-axis metric column(s). |
21
+ | `graph.series_order` | object[] | Per-series order/visibility `{ key, name, color, enabled }` (only with a breakout). |
22
+
23
+ **Stacking**
24
+
25
+ | Key | Type | Values | Default |
26
+ | ---------------------- | -------------- | ------------------------------------------------ | ---------------------------------------------- |
27
+ | `stackable.stack_type` | string \| null | `null`, `"stacked"`, `"normalized"` (100%) | `null` (`"stacked"` for `area` with >1 series) |
28
+ | `graph.split_panels` | boolean | each series in its own panel (excludes stacking) | `false` |
29
+
30
+ **Goal & trend**
31
+
32
+ | Key | Type | Default |
33
+ | ---------------------- | ------- | -------- |
34
+ | `graph.show_goal` | boolean | `false` |
35
+ | `graph.goal_value` | number | `0` |
36
+ | `graph.goal_label` | string | `"Goal"` |
37
+ | `graph.show_trendline` | boolean | `false` |
38
+
39
+ **Data labels**
40
+
41
+ | Key | Type | Values | Default |
42
+ | ------------------------------ | ------- | -------------------------------------------------- | --------- |
43
+ | `graph.show_values` | boolean | | `false` |
44
+ | `graph.label_value_frequency` | string | `"fit"`, `"all"` | `"fit"` |
45
+ | `graph.show_stack_values` | string | `"total"`, `"series"`, `"all"` (stacked bars only) | `"total"` |
46
+ | `graph.label_value_formatting` | string | `"auto"`, `"compact"`, `"full"` | `"auto"` |
47
+
48
+ **Axes**
49
+
50
+ | Key | Type | Values | Default |
51
+ | ----------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | ----------- |
52
+ | `graph.x_axis.scale` | string | `"timeseries"`, `"linear"`, `"pow"`, `"log"`, `"histogram"`, `"ordinal"` (subset by column type) | auto |
53
+ | `graph.y_axis.scale` | string | `"linear"`, `"pow"`, `"log"` | `"linear"` |
54
+ | `graph.x_axis.axis_enabled` | boolean \| string | `false`, `true`, `"compact"`, `"rotate-45"`, `"rotate-90"` | `true` |
55
+ | `graph.y_axis.axis_enabled` | boolean | | `true` |
56
+ | `graph.y_axis.unpin_from_zero` | boolean | don't force the Y axis through 0 | varies |
57
+ | `graph.y_axis.auto_range` | boolean | | `true` |
58
+ | `graph.y_axis.min` / `.max` | number | when `auto_range` is `false` | `0` / `100` |
59
+ | `graph.y_axis.auto_split` | boolean | split Y axis for multi-series | auto |
60
+ | `graph.x_axis.title_text` / `graph.y_axis.title_text` | string | axis label text (toggle with `*.labels_enabled`) | column name |
61
+
62
+ **Tooltip**
63
+
64
+ | Key | Type | Notes |
65
+ | ----------------------- | -------------------- | ---------------------------- |
66
+ | `graph.tooltip_columns` | string[] (col names) | extra columns shown on hover |
67
+
68
+ Per-series color/type/line-style live in `series_settings` (see below).
69
+
70
+ ## bar / line / area / combo
71
+
72
+ Use for comparing a measure across categories (`bar`) or change over time (`line`/`area`); `combo` overlays a line and bars for two related measures on different scales. **Use for:** >1 row, ≥2 columns, ≥1 dimension + ≥1 measure. **Required:** `graph.dimensions`, `graph.metrics`. Stacked-100% (`normalized`) is incompatible with a `log` y-scale.
73
+
74
+ ## scatter
75
+
76
+ Use for correlation between two numeric measures. Extra: `scatter.bubble` (numeric col name → bubble size). No stacking, data labels, or legend. **Required:** `graph.dimensions`, `graph.metrics`.
77
+
78
+ ## waterfall
79
+
80
+ Use for sequential additive contributions (start → +/− steps → total). **Use for:** exactly 1 dimension + 1 measure. X-scale can't be `pow`/`log`.
81
+
82
+ | Key | Type | Default |
83
+ | -------------------------- | ------- | ------- |
84
+ | `waterfall.increase_color` | string | accent1 |
85
+ | `waterfall.decrease_color` | string | accent3 |
86
+ | `waterfall.show_total` | boolean | `true` |
87
+ | `waterfall.total_color` | string | theme |
88
+
89
+ ## row
90
+
91
+ Horizontal bars — use when category labels are long or numerous. Here `graph.dimensions` is the y-axis (categories) and `graph.metrics` the x-axis (values). X-scale is `"ordinal"` only; no trend line, tooltip columns, or split panels. **Required:** `graph.dimensions`, `graph.metrics`.
92
+
93
+ ## boxplot
94
+
95
+ Use for distribution/spread/outliers, especially across several groups. Needs **unaggregated** rows. **Use for:** ≥3 columns, ≥2 dimensions, ≥1 measure. **Required:** `graph.dimensions`, `graph.metrics`. X-scale is `"ordinal"`.
96
+
97
+ | Key | Type | Values | Default |
98
+ | -------------------------- | ------- | ---------------------------------------------- | ------------ |
99
+ | `boxplot.whisker_type` | string | `"tukey"` (1.5×IQR), `"min-max"` | `"tukey"` |
100
+ | `boxplot.points_mode` | string | `"none"`, `"outliers"`, `"all"` | `"outliers"` |
101
+ | `boxplot.show_mean` | boolean | | `true` |
102
+ | `boxplot.show_values_mode` | string | `"median"`, `"all"` (when `graph.show_values`) | `"median"` |
103
+
104
+ ---
105
+
106
+ # Part-to-whole & single value — `pie`, `funnel`, `gauge`, `progress`, `scalar`, `smartscalar`
107
+
108
+ ## pie
109
+
110
+ Use for part-to-whole, one snapshot, ≤5 slices. **Use for:** ≥2 rows, ≥2 columns, ≥1 dimension + ≥1 measure. **Required:** `pie.dimension`, `pie.metric`.
111
+
112
+ | Key | Type | Values | Default |
113
+ | ------------------------ | ------------------ | ------------------------------------------------- | ---------------------- |
114
+ | `pie.metric` | string (col name) | the measure | first metric |
115
+ | `pie.dimension` | string \| string[] | the dimension; array = concentric rings (up to 3) | first dimension |
116
+ | `pie.show_legend` | boolean | | `true` |
117
+ | `pie.show_total` | boolean | total in the center | `true` |
118
+ | `pie.show_labels` | boolean | slice labels | `true` if >1 dimension |
119
+ | `pie.percent_visibility` | string | `"off"`, `"legend"`, `"inside"`, `"both"` | `"legend"` |
120
+ | `pie.decimal_places` | number | decimal places for percentages | auto |
121
+ | `pie.slice_threshold` | number (percent) | min slice % before grouping into "Other" | `2.5` |
122
+
123
+ (Set slice colors via the UI/escape hatch.)
124
+
125
+ ## funnel
126
+
127
+ Use for stage drop-off in an ordered, cumulative funnel. **Use for:** 2 columns (stage + value). **Required:** `funnel.dimension`, `funnel.metric`.
128
+
129
+ | Key | Type | Values | Default |
130
+ | ------------------ | ----------------- | ------------------------------------------ | ------------------------------------ |
131
+ | `funnel.dimension` | string (col name) | the step column | first dimension |
132
+ | `funnel.metric` | string (col name) | the step value | first metric |
133
+ | `funnel.type` | string | `"funnel"`, `"bar"` | `"funnel"` (`"bar"` if multi-series) |
134
+ | `funnel.rows` | object[] | step order/enable `{ key, name, enabled }` | data order |
135
+
136
+ ## gauge
137
+
138
+ Use for one value against colored target ranges. **Use for:** 1 row, 1 numeric column.
139
+
140
+ | Key | Type | Notes |
141
+ | ---------------- | -------- | ------------------------------------------------------------------ |
142
+ | `gauge.segments` | object[] | value ranges `{ min, max, color?, label? }` (`min`/`max` required) |
143
+
144
+ ## progress
145
+
146
+ Use for one value's progress toward a goal. **Use for:** 1 row, ≥1 numeric column.
147
+
148
+ | Key | Type | Values | Default |
149
+ | ---------------- | -------------------------- | ------------------------------------- | ------------- |
150
+ | `progress.value` | string (col name) | the numeric column (only if >1) | first numeric |
151
+ | `progress.goal` | number **or** string (col) | a literal target, or a numeric column | `0` |
152
+ | `progress.color` | string (color) | bar color | accent1 |
153
+
154
+ ## scalar
155
+
156
+ A single KPI — this **is** the "Number" viz (`display: scalar`). **Use for:** 1 row, 1 column. Number formatting (currency, decimals, prefix/suffix) is set per-column in `column_settings`, not here.
157
+
158
+ | Key | Type | Notes | Default |
159
+ | ----------------- | ----------------- | --------------------------------------------- | --------- |
160
+ | `scalar.field` | string (col name) | which column to show (only if >1 column) | first col |
161
+ | `scalar.segments` | object[] | color thresholds `{ min, max, color, label }` | `[]` |
162
+
163
+ ## smartscalar
164
+
165
+ Use for a value's change vs the previous period. **Use for:** one value grouped by a single **time** field.
166
+
167
+ | Key | Type | Default |
168
+ | --------------------------------- | ----------------- | --------------- |
169
+ | `scalar.field` | string (col name) | first numeric |
170
+ | `scalar.comparisons` | object[] | up to 3 (below) |
171
+ | `scalar.switch_positive_negative` | boolean | `false` |
172
+ | `scalar.compact_primary_number` | boolean | `false` |
173
+
174
+ Each `scalar.comparisons` entry is `{ id, type, … }`:
175
+
176
+ | `type` | Extra fields | Meaning |
177
+ | ------------------ | ----------------- | ---------------------- |
178
+ | `"previousPeriod"` | — | vs. the prior period |
179
+ | `"previousValue"` | — | vs. the previous value |
180
+ | `"periodsAgo"` | `value` | vs. N periods ago |
181
+ | `"staticNumber"` | `value`, `label` | vs. a fixed number |
182
+ | `"anotherColumn"` | `column`, `label` | vs. another column |
183
+
184
+ ```yaml
185
+ scalar.comparisons:
186
+ - { id: c1, type: previousPeriod }
187
+ - { id: c2, type: periodsAgo, value: 12 }
188
+ - { id: c3, type: staticNumber, value: 1000, label: Target }
189
+ ```
190
+
191
+ ---
192
+
193
+ # Tabular, geographic & flow — `table`, `pivot`, `object`, `map`, `sankey`
194
+
195
+ ## table
196
+
197
+ The universal fallback; use for precise values, many columns, or mixed types. Always renders.
198
+
199
+ | Key | Type | Notes | Default |
200
+ | ---------------------------- | -------- | ---------------------------------------------------------- | ------------------------------------------- |
201
+ | `table.columns` | object[] | column order + visibility `{ name, enabled }` | all columns |
202
+ | `table.column_formatting` | object[] | conditional formatting rules (below) | `[]` |
203
+ | `table.pivot` | boolean | simple in-table pivot (2 dims + 1 metric) | `true` only when 3 cols = 2 dims + 1 metric |
204
+ | `table.pivot_column` | string | dimension whose values become columns (when `table.pivot`) | auto |
205
+ | `table.cell_column` | string | column supplying pivot cell values (when `table.pivot`) | first metric |
206
+ | `table.pagination` | boolean | | `false` |
207
+ | `table.row_index` | boolean | show a row-index column | `false` |
208
+ | `table.freeze_columns` | boolean | freeze leading columns | `false` |
209
+ | `table.freeze_columns_count` | number | how many to freeze (when `freeze_columns`) | `1` |
210
+ | `table.freeze_rows` | boolean | freeze leading rows | `false` |
211
+ | `table.freeze_rows_count` | number | how many to freeze (when `freeze_rows`) | `1` |
212
+
213
+ Per-column titles, currency, links, alignment, and mini bars are set in `column_settings`.
214
+
215
+ **Conditional formatting (`table.column_formatting`)** — a list of rules, each `type: "single"` or `"range"`:
216
+
217
+ ```yaml
218
+ table.column_formatting:
219
+ - {
220
+ columns: [Total],
221
+ type: single,
222
+ operator: ">",
223
+ value: 100,
224
+ color: "#84BB4C",
225
+ highlight_row: false,
226
+ }
227
+ - {
228
+ columns: [Rating],
229
+ type: range,
230
+ colors: ["#ED6E6E", "#F9CF48", "#84BB4C"],
231
+ min_type: custom,
232
+ min_value: 1,
233
+ max_type: custom,
234
+ max_value: 5,
235
+ }
236
+ ```
237
+
238
+ `single` operators: `"="`, `"!="`, `"<"`, `">"`, `"<="`, `">="`, `"is-null"`, `"not-null"`, `"contains"`, `"does-not-contain"`, `"starts-with"`, `"ends-with"`, `"is-true"`, `"is-false"`. `range` `min_type`/`max_type`: `"custom"`, `"all"`, or `null`.
239
+
240
+ ## pivot
241
+
242
+ Use for a cross-tab of two dimensions. Needs an aggregated query built in the query builder (not native), on a database that supports pivots. The split and formatting use the `pivot_table.*` namespace; the totals toggles use `pivot.*`.
243
+
244
+ | Key | Type | Notes | Default |
245
+ | --------------------------------- | -------- | --------------------------------------------------------------- | ------- |
246
+ | `pivot_table.column_split` | object | `{ rows: [...names], columns: [...names], values: [...names] }` | auto |
247
+ | `pivot.show_row_totals` | boolean | | `true` |
248
+ | `pivot.show_column_totals` | boolean | | `true` |
249
+ | `pivot.condense_duplicate_totals` | boolean | hide duplicate total cells | `true` |
250
+ | `pivot_table.column_formatting` | object[] | conditional formatting on measure cells | — |
251
+
252
+ Per-column (under `column_settings[<key>]`): `pivot_table.column_sort_order` (`"ascending"`/`"descending"`), `pivot_table.column_show_totals` (boolean).
253
+
254
+ ## object
255
+
256
+ A single record's detail. `table.columns` (`{ name, enabled }`) picks which fields to show; per-column `column_settings` apply.
257
+
258
+ ## map
259
+
260
+ Three modes via `map.type`. **Region** (`"region"`) colors predefined areas; **pin** (`"pin"`) plots lat/long points; **grid** (`"grid"`) bins points. **Required:** region → `map.region`, `map.dimension`, `map.metric`; pin/grid → `map.latitude_column`, `map.longitude_column`.
261
+
262
+ | Key | Type | Values / notes | Mode |
263
+ | ---------------------------------------------------------- | ----------------- | ----------------------------------------------------------- | -------- |
264
+ | `map.type` | string | `"region"`, `"pin"`, `"grid"` | all |
265
+ | `map.region` | string | `"us_states"`, `"world_countries"`, or a custom-geojson key | region |
266
+ | `map.dimension` | string (col name) | the region column | region |
267
+ | `map.metric` | string (col name) | metric coloring the regions | region |
268
+ | `map.colors` | string[] | region color scale | region |
269
+ | `map.latitude_column` | string (col name) | latitude | pin/grid |
270
+ | `map.longitude_column` | string (col name) | longitude | pin/grid |
271
+ | `map.metric_column` | string (col name) | metric for heat/grid intensity | pin |
272
+ | `map.pin_type` | string | `"tiles"`, `"markers"`, `"grid"`, `"heat"` | pin |
273
+ | `map.heat.radius` / `.blur` / `.min-opacity` / `.max-zoom` | number | heatmap tuning | heat |
274
+
275
+ ## sankey
276
+
277
+ Use for flow volume between nodes. Needs distinct source and target columns forming an acyclic flow (≤150 unique nodes). **Use for:** ≥3 columns, ≥2 non-date dimensions, ≥1 measure. **Required:** `sankey.source`, `sankey.target`, `sankey.value`.
278
+
279
+ | Key | Type | Values | Default |
280
+ | ------------------------------- | ----------------- | -------------------------------- | ---------- |
281
+ | `sankey.source` | string (col name) | flow source | auto |
282
+ | `sankey.target` | string (col name) | flow target | auto |
283
+ | `sankey.value` | string (col name) | edge weight | auto |
284
+ | `sankey.node_align` | string | `"left"`, `"right"`, `"justify"` | `"left"` |
285
+ | `sankey.show_edge_labels` | boolean | | `false` |
286
+ | `sankey.label_value_formatting` | string | `"auto"`, `"compact"`, `"full"` | `"auto"` |
287
+ | `sankey.edge_color` | string | `"gray"`, `"source"`, `"target"` | `"source"` |
288
+
289
+ ---
290
+
291
+ # `column_settings` — per-column formatting
292
+
293
+ A map keyed by a JSON-encoded column reference, applying to `table`, `pie`, `object`, the cartesian charts, and more.
294
+
295
+ **Key forms:** prefer the name form `["name", "<output column name>"]` — it's what Metabase writes and is identical across API and portable forms. A legacy ref form `["ref", ["field", <id>, <opts>]]` exists for read-back; its inner field ref uses the **legacy order** (id second) with a numeric id in the API form — avoid it. In a JSON body the key is an escaped string: `"[\"name\",\"TOTAL\"]"`.
296
+
297
+ | Key | Type | Values | Applies to |
298
+ | -------------------- | ------------ | ------------------------------------------------------------ | --------------- |
299
+ | `column_title` | string | header override | all |
300
+ | `text_align` | string | `"left"`, `"right"`, `"middle"` | table |
301
+ | `view_as` | string\|null | `null` (text), `"link"`, `"email_link"`, `"image"`, `"auto"` | table |
302
+ | `link_text` | string | text for a link/email_link (supports `{{COLUMN}}`) | table |
303
+ | `link_url` | string | target URL (supports `{{COLUMN}}`) | table |
304
+ | `show_mini_bar` | boolean | inline bar in the cell | number |
305
+ | `text_wrapping` | boolean | wrap long text | string |
306
+ | `number_style` | string | `"decimal"`, `"currency"`, `"percent"`, `"scientific"` | number |
307
+ | `currency` | string | ISO code, e.g. `"USD"` | currency |
308
+ | `currency_style` | string | `"symbol"`, `"narrowSymbol"`, `"code"`, `"name"` | currency |
309
+ | `currency_in_header` | boolean | show currency in the header vs every cell | currency, table |
310
+ | `number_separators` | string | `".,"`, `", "`, `",."`, `"."`, `".’"` | number |
311
+ | `decimals` | number | fixed decimal places | number |
312
+ | `scale` | number | multiply the value by this factor | number |
313
+ | `prefix` / `suffix` | string | affixes around the value | number |
314
+ | `date_style` | string | moment.js format, e.g. `"MMMM D, YYYY"` | date |
315
+ | `date_separator` | string | `"/"`, `"-"`, `"."` | date |
316
+ | `date_abbreviate` | boolean | abbreviate day/month names | date |
317
+ | `time_enabled` | string\|null | `null`, `"minutes"`, `"seconds"`, `"milliseconds"` | date+time |
318
+ | `time_style` | string | e.g. `"h:mm A"`, `"HH:mm"` | date+time |
319
+ | `click_behavior` | object | per-column click behavior — **dashcards only** (below) | all |
320
+
321
+ ```yaml
322
+ column_settings:
323
+ '["name","TOTAL"]':
324
+ number_style: currency
325
+ currency: USD
326
+ decimals: 2
327
+ column_title: "Total Revenue"
328
+ '["name","CREATED_AT"]': { date_style: "MMMM D, YYYY", time_enabled: null }
329
+ '["name","EMAIL"]': { view_as: email_link, link_text: "Email {{NAME}}" }
330
+ ```
331
+
332
+ ---
333
+
334
+ # `series_settings` — per-series styling (cartesian)
335
+
336
+ A map under `series_settings`, keyed by series name: the metric column's name for a single series, or the breakout value for a broken-out series.
337
+
338
+ | Key | Type | Values | Default |
339
+ | ----------------------- | ------------- | ---------------------------------------------------- | --------------- |
340
+ | `title` | string | series label | column name |
341
+ | `color` | string | series color | palette |
342
+ | `display` | string | `"line"`, `"area"`, `"bar"` (per-series type, combo) | card default |
343
+ | `line.interpolate` | string | `"linear"`, `"cardinal"` (curved), `"step-after"` | `"linear"` |
344
+ | `line.style` | string | `"solid"`, `"dashed"`, `"dotted"` | `"solid"` |
345
+ | `line.size` | string | `"S"`, `"M"`, `"L"` | `"M"` |
346
+ | `line.marker_enabled` | boolean\|null | `null` (auto), `true`, `false` | `null` |
347
+ | `line.missing` | string | `"zero"`, `"none"`, `"interpolate"` | `"interpolate"` |
348
+ | `axis` | string\|null | `null` (auto), `"left"`, `"right"` | `null` |
349
+ | `show_series_values` | boolean | value labels for this series | inherits chart |
350
+ | `show_series_trendline` | boolean | trend line for this series | inherits chart |
351
+
352
+ `line.*` keys apply only when the series `display` is `line`/`area`.
353
+
354
+ ```yaml
355
+ series_settings:
356
+ Revenue: { display: line, color: "#509EE3", axis: left, show_series_values: true }
357
+ ```
358
+
359
+ ---
360
+
361
+ # Virtual cards (dashcards only, `card_id: null`)
362
+
363
+ Text/heading/link/iframe tiles carry `visualization_settings.virtual_card.display`:
364
+
365
+ | `virtual_card.display` | Extra keys |
366
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------- |
367
+ | `heading` | `text` (the heading string) |
368
+ | `text` | `text` (markdown; `{{param}}` placeholders wired via dashcard `parameter_mappings`, target `[text-tag, name]`) |
369
+ | `link` | `link.url`, or `link.entity` `{ id, model }`, model ∈ `question`/`dashboard`/`collection`/`database`/`table` |
370
+ | `iframe` | `iframe` (the `<iframe …>` HTML string) |
371
+
372
+ ```yaml
373
+ visualization_settings:
374
+ virtual_card: { display: text }
375
+ text: "**Bold** and _italic_ markdown content"
376
+ ```
377
+
378
+ ---
379
+
380
+ # Click behavior (dashcards only)
381
+
382
+ Click behavior is a **dashboard** feature — author it in a **dashcard's** `visualization_settings` (whole-card) or its `column_settings[<key>].click_behavior` (per column), **not** in a saved card's own `visualization_settings`. There is no query-builder UI for it, and the interactive types need the dashboard around them: `crossfilter` reads the dashboard's parameters, and links to a `question`/`dashboard` resolve against the dashboard's target entities — both do nothing on a standalone card. (A bare URL link is the only type not strictly gated by dashboard context, but it's still only authored on dashcards.)
383
+
384
+ `type` is `actionMenu` (default drill-through menu), `crossfilter` (filter the dashboard with the clicked value), or `link` (go to a URL, question, or dashboard).
385
+
386
+ ```yaml
387
+ # Link to a URL — {{column}} = clicked row value, {{filter:param}} = dashboard parameter
388
+ click_behavior:
389
+ type: link
390
+ linkType: url
391
+ linkTemplate: "https://example.com/orders/{{ORDER_ID}}?status={{filter:status}}"
392
+
393
+ # Link to another dashboard/question — targetId is the target's entity_id
394
+ click_behavior:
395
+ type: link
396
+ linkType: dashboard # or "question"
397
+ targetId: Q_jD-f-9clKLFZ2TfUG2h
398
+ parameterMapping:
399
+ target-param-uuid:
400
+ id: target-param-uuid
401
+ source: { id: USER_ID, name: User ID, type: column }
402
+ target: { id: target-param-uuid, type: parameter }
403
+
404
+ # Crossfilter — map a clicked column to dashboard parameters
405
+ click_behavior:
406
+ type: crossfilter
407
+ parameterMapping:
408
+ param-uuid:
409
+ id: param-uuid
410
+ source: { id: CATEGORY, name: Category, type: column }
411
+ target: { id: param-uuid, type: parameter }
412
+ ```
413
+
414
+ In `parameterMapping`, `source` is `{ id, name, type }` (type `"column"`/`"parameter"`) and `target` is `{ id, type }` (type `"parameter"`/`"dimension"`/`"variable"`); a `dimension` target also carries a `dimension` array.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: metabase-cli
3
- description: Drive a Metabase instance from the terminal via the `mb` CLI — auth, databases, cards, dashboards, transforms, queries, search, git-sync, Enterprise workspaces. Discovery entry; load the full guide with `mb skills get core`.
3
+ description: Drive a Metabase instance from the terminal via the `mb` CLI — auth, databases, cards, dashboards, transforms, queries, search, git-sync. Discovery entry; load the full guide with `mb skills get core`.
4
4
  allowed-tools: Bash, Read, Write, Edit, AskUserQuestion
5
5
  hidden: true
6
6
  ---
@@ -1,11 +0,0 @@
1
- import "./command-augment-BH9qgQ5u.mjs";
2
- import "./error-C9S6PN3-.mjs";
3
- import "./runtime-Dmv5VtUK.mjs";
4
- import "./capabilities-7e9MgquN.mjs";
5
- import "./render-CfznwleY.mjs";
6
- import "./parse-id-lk_K-CEF.mjs";
7
- import "./poll-task-Cooi0lQV.mjs";
8
- import "./poll-DHKDpCiq.mjs";
9
- import { SyncSettingsUpdateResult, add_collection_default, setCollectionRemoteSynced, syncSettingsUpdateView } from "./add-collection-BU8r3r2M.mjs";
10
-
11
- export { add_collection_default as default };
@@ -1,19 +0,0 @@
1
- import { defineCommand } from "citty";
2
-
3
- //#region src/commands/auth/index.ts
4
- var auth_default = defineCommand({
5
- meta: {
6
- name: "auth",
7
- description: "Authenticate against a Metabase instance"
8
- },
9
- default: "login",
10
- subCommands: {
11
- login: () => import("./login-Bm2AnCez.mjs").then((m) => m.default),
12
- status: () => import("./status-B0_MiZEf.mjs").then((m) => m.default),
13
- list: () => import("./list-D4sFiqX8.mjs").then((m) => m.default),
14
- logout: () => import("./logout-BlyRJODO.mjs").then((m) => m.default)
15
- }
16
- });
17
-
18
- //#endregion
19
- export { auth_default as default };