@lightdash/cli 1.13.0 → 1.15.0

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.
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: reusable-visualization
3
+ description: Build ONE reusable chart visualization component that receives its data and its settings from the host application instead of fetching them, and declares the fields and config options the host exposes to viewers. Use this whenever a single chart component is reused across many different queries rather than built for one — in Lightdash, the `data_app_viz` app template, also called a "viz". Covers the `useVizContext()` hook, the declaration contract, the config-option vocabulary, and series colours.
4
+ ---
5
+
6
+ # Reusable visualization
7
+
8
+ This is ONE reusable chart component, not an app. It runs no query and owns no explore.
9
+ Lightdash runs the query, then hands the component the result rows plus a mapping from the
10
+ field names the component declared to the columns in those rows. The same component is
11
+ reused across many different queries — every contract below follows from that: it cannot
12
+ hardcode column names, so it declares a field mapping; it cannot hardcode display choices
13
+ for queries it has never seen, so it declares config options.
14
+
15
+ No dashboard, navigation, multiple panels, filters or page chrome. Recharts, echarts, D3 or
16
+ plain SVG all work.
17
+
18
+ Everything the viewer can reach later exists only because the component declared it: the
19
+ columns they map, and every control in the chart's config panel. A literal hardcoded
20
+ instead stays frozen until somebody regenerates the whole viz.
21
+
22
+ ## The hook
23
+
24
+ `useVizContext()` from `@lightdash/query-sdk` is the only source of data and settings. Do
25
+ not add a message listener and do not fetch anything.
26
+
27
+ ```tsx
28
+ const { fieldMapping, rows, options, colorPalette, ready } = useVizContext();
29
+ ```
30
+
31
+ - `fieldMapping` — `Record<string, string>`: the field name you declared → the query field
32
+ id it is bound to. Read cells with `getFormatted(row, fieldId)` (display text) and
33
+ `getRaw(row, fieldId)` (raw value).
34
+ - `rows` — the host-fetched result rows, keyed by query field id.
35
+ - `options` — `Record<string, boolean | number | string>`: the current value of each config
36
+ option you declared (the viewer's choice, else your declared `default`).
37
+ - `colorPalette` — `string[]`: the Lightdash palette resolved for this chart. Always pushed,
38
+ whether or not you declared `colorPalette`. Empty only when the host resolved none.
39
+ - `ready` — false until the first context arrives.
40
+
41
+ Read all five. Ignoring `options` and `colorPalette` is the most common way to build a viz
42
+ nobody can configure.
43
+
44
+ ### These app-level APIs do not apply to a viz
45
+
46
+ The sandbox skill (`/app/skill.md`) is written for data apps, which do run queries. A
47
+ reusable visualization does not. Do **not** use `useLightdash()`, `filtersFor(EXPLORE)`,
48
+ `addFilter({...})`, or `format(row, fieldName)` — there is no explore to query or filter,
49
+ and formatting arrives pre-computed via `getFormatted`. Where this skill and the sandbox
50
+ skill differ, this skill wins for a viz. Everything else in the sandbox skill (React,
51
+ shadcn, charting, theming, floating surfaces, screenshots) still applies.
52
+
53
+ ## Rendering rules
54
+
55
+ - **Fill the viewport.** Give the root element `height: 100vh` (or `position: fixed; inset: 0`),
56
+ NOT `height: 100%` — that collapses to a 0-height invisible box unless every ancestor also
57
+ sets a height, leaving auto-sizing charts like recharts `<ResponsiveContainer>` nothing to
58
+ measure. Confirm the chart actually renders and isn't a blank box.
59
+ - **Placeholder.** Show a clearly visible placeholder (readable, good contrast — never
60
+ near-white on white) while `!ready`, when a required field is unmapped, or when there are
61
+ no rows.
62
+ - **Fundamentals.** Clear axes and labels, readable spacing, a tooltip on hover.
63
+ Give every axis a `tickFormatter` (compact numbers, shortened labels) or `hide`
64
+ it — raw ticks overflow and overlap on a chart the viewer can resize.
65
+
66
+ ## Series colours
67
+
68
+ Never hardcode series hex values and never hand-pick a palette of your own. Colour series
69
+ with `colorPalette[i % colorPalette.length]`, and declare `colorPalette` in your output so
70
+ the viewer gets the palette picker. Keep a small fallback array in your own code for the
71
+ case where `colorPalette` is empty.
72
+
73
+ This is the same rule as the sandbox skill's "chart series colors must come from
74
+ `CHART_COLORS`" and `references/d3.md`'s "never hand-pick palettes" — the same Lightdash
75
+ palette, delivered differently. An app imports `CHART_COLORS`; a viz reads the resolved
76
+ colours off the hook, because the viewer picks the palette per chart. In a viz, use the
77
+ hook, not `CHART_COLORS`.
78
+
79
+ ## The declaration
80
+
81
+ Alongside the component you emit one structured declaration — as **structured output, not a
82
+ file**. It has three parts: `fields`, `configOptions` and `colorPalette`. Lightdash builds
83
+ the field-mapping UI and the chart config panel from it, so the component is unusable
84
+ without it.
85
+
86
+ The correspondence is exact in both directions: **every key read from `fieldMapping` or
87
+ `options` is declared, and everything declared is read.** A declared option nothing reads is
88
+ a dead control the viewer can move with no effect.
89
+
90
+ ### `fields`
91
+
92
+ One entry per data column the component reads:
93
+
94
+ - `name` — the key read from `fieldMapping`. Unique, no spaces.
95
+ - `label` — human label shown in the mapping UI.
96
+ - `type` — `dimension` (a category/grouping column), `metric` (a numeric measure), or
97
+ `series` (a dimension used to split or colour the chart).
98
+ - `required` — `false` only when the chart still renders with this field unmapped.
99
+
100
+ ### `configOptions`
101
+
102
+ One entry per setting the viewer can change without regenerating the viz. Every option is a
103
+ whole-viz value applying to the entire chart — there is no per-series option. To colour
104
+ series individually, index into `colorPalette` by series position.
105
+
106
+ Every option has:
107
+
108
+ - `name` — the key read from `options`. Unique, no spaces.
109
+ - `label` — human label shown next to the control.
110
+ - `group` — optional tab name. Options sharing a `group` are rendered in the same config
111
+ tab; ungrouped options share a default tab.
112
+ - `default` — REQUIRED on every option. The value the viz uses until the viewer changes it;
113
+ its shape follows `type`. Use the value you would otherwise have hardcoded.
114
+ - `type` — exactly one of these five, no others:
115
+
116
+ | `type` | control | `default` | extra keys |
117
+ |---|---|---|---|
118
+ | `boolean` | switch | `true` / `false` | — |
119
+ | `select` | dropdown | one of the declared choice `value`s (a string) | `choices`: array of `{ "value": "...", "label": "..." }`, at least one entry |
120
+ | `number` | number input | a number | `min`, `max` — both optional numbers |
121
+ | `text` | single-line text input | a string | — |
122
+ | `color` | single colour | a hex string, e.g. `"#7162FF"` | — |
123
+
124
+ Series colours are not in this list. They are declared separately, on `colorPalette`.
125
+
126
+ ### `colorPalette`
127
+
128
+ Whether the viewer gets the standard Lightdash palette picker. Declare
129
+ `{ "group": "..." }` when your component colours anything from `colorPalette`, or `null`
130
+ when it colours nothing. `group` is optional and works like an option's: the picker joins
131
+ that tab, and gets a tab of its own when no option shares the name.
132
+
133
+ This is not a config option. There is one palette per chart, it has no `name` and no
134
+ `default`, and its colours arrive on `colorPalette` — never on `options`.
135
+
136
+ ## Worked example
137
+
138
+ Component and declaration lining up. Your chart will differ; the correspondence must not.
139
+
140
+ ```tsx
141
+ import { useVizContext, getFormatted, getRaw } from '@lightdash/query-sdk';
142
+ import { Bar, BarChart, Cell, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
143
+
144
+ function Chart() {
145
+ const { fieldMapping, rows, options, colorPalette, ready } = useVizContext();
146
+ if (!ready) return <div style={{ height: '100vh' }}>Loading…</div>;
147
+ const catField = fieldMapping['category']; // your field name -> column id
148
+ const valField = fieldMapping['value'];
149
+ const showLabels = options['showLabels'] as boolean; // your option name -> current value
150
+ const maxBars = options['maxBars'] as number;
151
+ const colors = colorPalette.length ? colorPalette : ['#7162FF', '#1A1B1E'];
152
+ const data = rows.slice(0, maxBars).map((row) => ({
153
+ label: getFormatted(row, catField), // display text, e.g. "Completed"
154
+ value: Number(getRaw(row, valField) ?? 0), // raw number
155
+ }));
156
+ return (
157
+ // The root fills the viewport, so ResponsiveContainer has a height to measure.
158
+ <div style={{ height: '100vh' }}>
159
+ <ResponsiveContainer width="100%" height="100%">
160
+ <BarChart data={data}>
161
+ <XAxis
162
+ dataKey="label"
163
+ tickFormatter={(v) => (v.length > 12 ? `${v.slice(0, 11)}…` : v)}
164
+ />
165
+ <YAxis tickFormatter={(v) => v.toLocaleString()} />
166
+ <Tooltip />
167
+ <Bar dataKey="value">
168
+ {data.map((d, i) => <Cell key={d.label} fill={colors[i % colors.length]} />)}
169
+ {showLabels && <LabelList dataKey="value" position="top" />}
170
+ </Bar>
171
+ </BarChart>
172
+ </ResponsiveContainer>
173
+ </div>
174
+ );
175
+ }
176
+ ```
177
+
178
+ The declaration that component emits is exactly:
179
+
180
+ ```
181
+ fields: [{ "name": "category", "label": "Category", "type": "dimension", "required": true },
182
+ { "name": "value", "label": "Value", "type": "metric", "required": true }]
183
+ configOptions: [{ "name": "showLabels", "label": "Show value labels", "group": "Labels", "type": "boolean", "default": true },
184
+ { "name": "maxBars", "label": "Max bars", "type": "number", "default": 10, "min": 1, "max": 50 }]
185
+ colorPalette: {}
186
+ ```
187
+
188
+ ## Final pass, before you finish
189
+
190
+ Re-read the component you just wrote and list every literal in it: each colour, each
191
+ true/false you chose, each number, each string you typed into the chart. Take the list one
192
+ entry at a time and ask "would a viewer plausibly want this different?". This is the
193
+ minimum, not a menu:
194
+
195
+ - every element you chose to show or hide (value labels, legend, gridlines, an axis) → `boolean`
196
+ - every variant you picked between (vertical/horizontal, grouped/stacked, curve style) → `select`
197
+ - every number you chose (bar width, max rows, decimal places, a threshold) → `number`
198
+ - every string you wrote into the chart (title, axis label, empty-state text) → `text`
199
+ - every accent colour that is not a series colour (a target line, a highlight) → `color`
200
+ - the series colours, whenever the chart draws more than one series → `colorPalette`
201
+
202
+ Where the answer is yes, make that literal the option's `default`, declare the option, and
203
+ read the option in its place. Leave it hardcoded only where changing it would break the
204
+ chart.
205
+
206
+ Then check both directions: every key you read from `options` is declared, and every option
207
+ you declared is read somewhere. `colorPalette` is declared when you colour from
208
+ `colorPalette` — it is never read from `options`.
@@ -2,6 +2,8 @@
2
2
 
3
3
  You are building a React data app that queries the Lightdash semantic layer. This file is your reference for the environment, SDK, and data model.
4
4
 
5
+ **Building a single reusable chart rather than an app?** Read `.claude/skills/reusable-visualization/SKILL.md` before writing any code. A reusable visualization runs no query of its own — the host hands it rows, a field mapping, config option values and a colour palette — so the data and filter APIs below do not apply to it. That skill is the contract for those builds and overrides this guide wherever the two differ. Everything else here (environment, components, visual design) still applies.
6
+
5
7
  ## Iteration mindset
6
8
 
7
9
  This pipeline is built for iteration — the user refines the app with follow-up prompts, and you have the full conversation history on every iteration. **Favor a responsive first build over upfront perfection.** Hit the core ask and ship; let the user tell you what to add.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "unique-names-generator": "4.7.1",
42
42
  "uuid": "11.1.1",
43
43
  "yaml": "2.8.3",
44
- "@lightdash/common": "1.13.0",
45
- "@lightdash/warehouses": "1.13.0"
44
+ "@lightdash/common": "1.15.0",
45
+ "@lightdash/warehouses": "1.15.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",