@lightdash/cli 1.12.0 → 1.14.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.
|
|
3
|
+
"version": "1.14.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.
|
|
45
|
-
"@lightdash/warehouses": "1.
|
|
44
|
+
"@lightdash/common": "1.14.0",
|
|
45
|
+
"@lightdash/warehouses": "1.14.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/inquirer": "8.2.4",
|