@matthieumordrel/chart-studio 0.4.0 → 0.5.3
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.
- package/README.md +10 -768
- package/dist/_internal.d.mts +9 -0
- package/dist/_internal.mjs +9 -0
- package/dist/core/chart-capabilities.d.mts +5 -0
- package/dist/core/chart-capabilities.mjs +9 -0
- package/dist/core/config-utils.mjs +2 -1
- package/dist/core/data-label-defaults.d.mts +92 -0
- package/dist/core/data-label-defaults.mjs +4 -0
- package/dist/core/date-range-presets.d.mts +43 -1
- package/dist/core/date-range-presets.mjs +1 -1
- package/dist/core/date-utils.d.mts +26 -0
- package/dist/core/formatting.d.mts +49 -0
- package/dist/core/formatting.mjs +32 -10
- package/dist/core/index.d.mts +5 -3
- package/dist/core/metric-utils.d.mts +18 -2
- package/dist/core/metric-utils.mjs +1 -1
- package/dist/core/types.d.mts +2 -2
- package/dist/core/use-chart.mjs +1 -1
- package/dist/core/use-dashboard.mjs +1 -1
- package/dist/index.d.mts +7 -4
- package/dist/index.mjs +4 -1
- package/package.json +10 -41
- package/LICENSE +0 -21
- package/dist/ui/chart-axis-ticks.mjs +0 -65
- package/dist/ui/chart-canvas.d.mts +0 -40
- package/dist/ui/chart-canvas.mjs +0 -790
- package/dist/ui/chart-context.d.mts +0 -101
- package/dist/ui/chart-context.mjs +0 -117
- package/dist/ui/chart-date-range-badge.d.mts +0 -20
- package/dist/ui/chart-date-range-badge.mjs +0 -49
- package/dist/ui/chart-date-range-panel.d.mts +0 -18
- package/dist/ui/chart-date-range-panel.mjs +0 -126
- package/dist/ui/chart-date-range.d.mts +0 -20
- package/dist/ui/chart-date-range.mjs +0 -67
- package/dist/ui/chart-debug.d.mts +0 -21
- package/dist/ui/chart-debug.mjs +0 -173
- package/dist/ui/chart-dropdown.mjs +0 -92
- package/dist/ui/chart-filters-panel.d.mts +0 -26
- package/dist/ui/chart-filters-panel.mjs +0 -258
- package/dist/ui/chart-filters.d.mts +0 -18
- package/dist/ui/chart-filters.mjs +0 -48
- package/dist/ui/chart-group-by-selector.d.mts +0 -16
- package/dist/ui/chart-group-by-selector.mjs +0 -32
- package/dist/ui/chart-metric-panel.d.mts +0 -25
- package/dist/ui/chart-metric-panel.mjs +0 -172
- package/dist/ui/chart-metric-selector.d.mts +0 -16
- package/dist/ui/chart-metric-selector.mjs +0 -50
- package/dist/ui/chart-select.mjs +0 -61
- package/dist/ui/chart-source-switcher.d.mts +0 -24
- package/dist/ui/chart-source-switcher.mjs +0 -56
- package/dist/ui/chart-time-bucket-selector.d.mts +0 -17
- package/dist/ui/chart-time-bucket-selector.mjs +0 -37
- package/dist/ui/chart-toolbar-overflow.d.mts +0 -28
- package/dist/ui/chart-toolbar-overflow.mjs +0 -223
- package/dist/ui/chart-toolbar.d.mts +0 -33
- package/dist/ui/chart-toolbar.mjs +0 -60
- package/dist/ui/chart-type-selector.d.mts +0 -19
- package/dist/ui/chart-type-selector.mjs +0 -168
- package/dist/ui/chart-x-axis-selector.d.mts +0 -16
- package/dist/ui/chart-x-axis-selector.mjs +0 -28
- package/dist/ui/index.d.mts +0 -19
- package/dist/ui/index.mjs +0 -18
- package/dist/ui/percent-stacked.mjs +0 -36
- package/dist/ui/theme.css +0 -67
- package/dist/ui/toolbar-types.d.mts +0 -7
- package/dist/ui/toolbar-types.mjs +0 -83
package/README.md
CHANGED
|
@@ -1,785 +1,27 @@
|
|
|
1
|
-
# chart-studio
|
|
1
|
+
# @matthieumordrel/chart-studio
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Early alpha. Active work in progress. Not recommended for production use yet.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- use the **optional UI layer** if you also want ready-made controls and a Recharts canvas
|
|
5
|
+
Headless, composable charting for React.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Start Here
|
|
11
|
-
|
|
12
|
-
Choose the path that matches your app:
|
|
13
|
-
|
|
14
|
-
### 1. Headless core
|
|
15
|
-
|
|
16
|
-
Use this if you already have your own design system or chart renderer.
|
|
17
|
-
|
|
18
|
-
You get:
|
|
19
|
-
|
|
20
|
-
- `useChart`
|
|
21
|
-
- optional `schema` via `defineDataset(...).chart(...)`
|
|
22
|
-
- transformed chart data
|
|
23
|
-
- filtering, grouping, metrics, and time bucketing logic
|
|
24
|
-
|
|
25
|
-
Requirements:
|
|
26
|
-
|
|
27
|
-
- `react` >= 18.2.0
|
|
28
|
-
|
|
29
|
-
Install:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
bun add @matthieumordrel/chart-studio react
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Import from:
|
|
36
|
-
|
|
37
|
-
```tsx
|
|
38
|
-
import { useChart } from '@matthieumordrel/chart-studio'
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 2. Ready-made UI
|
|
42
|
-
|
|
43
|
-
Use this if you want the package to render the controls and chart for you.
|
|
44
|
-
|
|
45
|
-
You get:
|
|
46
|
-
|
|
47
|
-
- everything from the headless core
|
|
48
|
-
- `<Chart>`
|
|
49
|
-
- `<ChartToolbar>`
|
|
50
|
-
- `<ChartCanvas>`
|
|
51
|
-
- granular UI controls from `@matthieumordrel/chart-studio/ui`
|
|
52
|
-
|
|
53
|
-
Requirements:
|
|
54
|
-
|
|
55
|
-
- `react` >= 18.2.0
|
|
56
|
-
- `recharts` >= 3.0.0 (v2 is **not** supported)
|
|
57
|
-
- `lucide-react` >= 0.577.0 (optional, for toolbar icons)
|
|
7
|
+
Use this package when you want chart state, filtering, grouping, metrics, time bucketing, transformed data, and the model/dashboard APIs without the optional UI layer.
|
|
58
8
|
|
|
59
9
|
Install:
|
|
60
10
|
|
|
61
11
|
```bash
|
|
62
|
-
bun add @matthieumordrel/chart-studio react
|
|
12
|
+
bun add @matthieumordrel/chart-studio@alpha react
|
|
63
13
|
```
|
|
64
14
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```css
|
|
68
|
-
@import 'tailwindcss';
|
|
69
|
-
@import '@matthieumordrel/chart-studio/ui/theme.css';
|
|
70
|
-
```
|
|
15
|
+
Current prereleases are published under the `alpha` dist-tag on npm.
|
|
71
16
|
|
|
72
17
|
Import from:
|
|
73
18
|
|
|
74
19
|
```tsx
|
|
75
|
-
import { useChart
|
|
76
|
-
import { Chart, ChartToolbar, ChartCanvas } from '@matthieumordrel/chart-studio/ui'
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Smallest Working Example (Single Source)
|
|
80
|
-
|
|
81
|
-
```tsx
|
|
82
|
-
import { useChart } from '@matthieumordrel/chart-studio'
|
|
83
|
-
import { Chart, ChartToolbar, ChartCanvas } from '@matthieumordrel/chart-studio/ui'
|
|
84
|
-
import { data } from './data.json'
|
|
85
|
-
|
|
86
|
-
export function JobsChart() {
|
|
87
|
-
const chart = useChart({ data })
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<Chart chart={chart}>
|
|
91
|
-
<ChartToolbar />
|
|
92
|
-
<ChartCanvas height={320} />
|
|
93
|
-
</Chart>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## How It Works
|
|
99
|
-
|
|
100
|
-
1. Pass your raw data to `useChart()`.
|
|
101
|
-
2. Add an optional `schema` with `defineDataset<Row>().chart(...)` when you need labels, type overrides, derived columns, or control restrictions (allowed metrics, groupings, chart types, etc.).
|
|
102
|
-
3. Either render your own UI from the returned state, or use the components from `@matthieumordrel/chart-studio/ui`.
|
|
103
|
-
|
|
104
|
-
## Stable Single-Chart Contract
|
|
105
|
-
|
|
106
|
-
For the simple case, the public contract is:
|
|
107
|
-
|
|
108
|
-
- `useChart({ data })` stays the zero-config path
|
|
109
|
-
- `useChart({ data, schema })` is the explicit single-chart path
|
|
110
|
-
- `defineDataset<Row>().chart(...)` is the single explicit way to build that schema
|
|
111
|
-
- `.columns(...)` is the authoring entry point: override raw fields, exclude fields, and add derived columns
|
|
112
|
-
- raw fields you do not mention in `.columns(...)` still infer normally unless you exclude them
|
|
113
|
-
- `xAxis`, `groupBy`, `filters`, `metric`, `chartType`, `timeBucket`, and `connectNulls` restrict that one chart's public controls
|
|
114
|
-
- `inputs` is an additive escape hatch for externally controlled data-scope state; it does not replace the simple `useChart({ data })` or `useChart({ data, schema })` path
|
|
115
|
-
- pass the builder directly to `useChart(...)`, or call `.build()` if you need the plain schema object
|
|
116
|
-
|
|
117
|
-
## Choose the Right Boundary
|
|
118
|
-
|
|
119
|
-
There are two different scaling paths in `chart-studio`:
|
|
120
|
-
|
|
121
|
-
### Independent dataset(s)
|
|
122
|
-
|
|
123
|
-
Stay on the single-chart path when each chart reads one flat row shape, even if
|
|
124
|
-
one screen renders several unrelated charts.
|
|
125
|
-
|
|
126
|
-
Use:
|
|
127
|
-
|
|
128
|
-
- `useChart({ data })` for zero-config charts
|
|
129
|
-
- `defineDataset<Row>().chart(...)` when one chart owns its own explicit contract
|
|
130
|
-
- `defineDataset<Row>()` when several charts should reuse one row contract
|
|
131
|
-
- `useChart({ sources: [...] })` only for source-switching inside one chart
|
|
132
|
-
|
|
133
|
-
Stop here when:
|
|
134
|
-
|
|
135
|
-
- charts do not need relationship-aware shared filters
|
|
136
|
-
- datasets are already flattened for the charts that consume them
|
|
137
|
-
- each chart can execute honestly against one dataset at a time
|
|
138
|
-
|
|
139
|
-
### Related dashboard
|
|
140
|
-
|
|
141
|
-
Move up to the model + dashboard path when several datasets are structurally
|
|
142
|
-
related and you want shared filters, referential validation, or safe
|
|
143
|
-
lookup-preserving cross-dataset fields.
|
|
144
|
-
|
|
145
|
-
Recommended path for new dashboard work:
|
|
146
|
-
|
|
147
|
-
1. `defineDataModel().dataset(...).infer(...)`
|
|
148
|
-
2. `model.chart(...)` for lookup-preserving charts
|
|
149
|
-
3. `model.materialize(...)` only when the chart grain changes
|
|
150
|
-
4. `defineDashboard(model)`
|
|
151
|
-
5. `useDashboard(...)`
|
|
152
|
-
|
|
153
|
-
## Authoring Layers
|
|
154
|
-
|
|
155
|
-
### 1. Single-chart explicit path
|
|
156
|
-
|
|
157
|
-
Use `defineDataset<Row>().chart(...)` when one chart owns its own explicit contract:
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
const schema = defineDataset<Job>()
|
|
161
|
-
.columns((c) => [
|
|
162
|
-
c.date('createdAt'),
|
|
163
|
-
c.category('ownerName'),
|
|
164
|
-
c.number('salary')
|
|
165
|
-
])
|
|
166
|
-
.chart()
|
|
167
|
-
.xAxis((x) => x.allowed('createdAt'))
|
|
168
|
-
.metric((m) => m.aggregate('salary', 'sum'))
|
|
169
|
-
|
|
170
|
-
const chart = useChart({ data: jobs, schema })
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
This is the single explicit path for declaring a chart schema. The dataset
|
|
174
|
-
owns the column contract, and `.chart(...)` layers chart-specific control
|
|
175
|
-
restrictions on top.
|
|
176
|
-
|
|
177
|
-
### 2. Dataset-first reuse
|
|
178
|
-
|
|
179
|
-
Use `defineDataset<Row>()` when several charts should share one `.columns(...)`
|
|
180
|
-
contract and one optional declared key:
|
|
181
|
-
|
|
182
|
-
```tsx
|
|
183
|
-
import { defineDataset, useChart } from '@matthieumordrel/chart-studio'
|
|
184
|
-
|
|
185
|
-
const jobs = defineDataset<Job>()
|
|
186
|
-
.key('id')
|
|
187
|
-
.columns((c) => [
|
|
188
|
-
c.field('id'),
|
|
189
|
-
c.field('ownerId'),
|
|
190
|
-
c.date('createdAt', { label: 'Created' }),
|
|
191
|
-
c.number('salary', { format: 'currency' }),
|
|
192
|
-
c.derived.category('salaryBand', {
|
|
193
|
-
label: 'Salary Band',
|
|
194
|
-
accessor: (row) => (row.salary >= 100_000 ? 'High' : 'Base')
|
|
195
|
-
})
|
|
196
|
-
])
|
|
197
|
-
|
|
198
|
-
const jobsByMonth = jobs
|
|
199
|
-
.chart('jobsByMonth')
|
|
200
|
-
.xAxis((x) => x.allowed('createdAt').default('createdAt'))
|
|
201
|
-
.groupBy((g) => g.allowed('salaryBand'))
|
|
202
|
-
.metric((m) => m.count().aggregate('salary', 'sum'))
|
|
203
|
-
|
|
204
|
-
const chart = useChart({ data: jobsData, schema: jobsByMonth })
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Rules for the dataset-first path:
|
|
208
|
-
|
|
209
|
-
- dataset `.columns(...)` is the canonical reusable meaning of columns
|
|
210
|
-
- `dataset.chart(...)` provides the chart-definition surface for control restrictions
|
|
211
|
-
- `dataset.chart(...)` inherits dataset columns, so charts do not reopen `.columns(...)`
|
|
212
|
-
- declared dataset keys can be validated at runtime with `dataset.validateData(data)` or `validateDatasetData(dataset, data)`
|
|
213
|
-
|
|
214
|
-
### 3. Model-level linked data
|
|
215
|
-
|
|
216
|
-
Use `defineDataModel()` when datasets are related and you want the model to own
|
|
217
|
-
safe inference, validation, and reusable dashboard semantics:
|
|
218
|
-
|
|
219
|
-
```tsx
|
|
220
|
-
import {
|
|
221
|
-
defineDataModel,
|
|
222
|
-
defineDataset,
|
|
223
|
-
} from '@matthieumordrel/chart-studio'
|
|
224
|
-
|
|
225
|
-
const hiringModel = defineDataModel()
|
|
226
|
-
.dataset('jobs', defineDataset<Job>()
|
|
227
|
-
.key('id')
|
|
228
|
-
.columns((c) => [
|
|
229
|
-
c.date('createdAt'),
|
|
230
|
-
c.category('status'),
|
|
231
|
-
c.number('salary', { format: 'currency' }),
|
|
232
|
-
]))
|
|
233
|
-
.dataset('owners', defineDataset<Owner>()
|
|
234
|
-
.key('id')
|
|
235
|
-
.columns((c) => [
|
|
236
|
-
c.category('name', { label: 'Owner' }),
|
|
237
|
-
c.category('region'),
|
|
238
|
-
]))
|
|
239
|
-
.infer({
|
|
240
|
-
relationships: true,
|
|
241
|
-
attributes: true,
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
const jobsByOwner = hiringModel.chart('jobsByOwner', (chart) =>
|
|
245
|
-
chart
|
|
246
|
-
.xAxis((x) => x.allowed('jobs.createdAt', 'jobs.owner.name').default('jobs.owner.name'))
|
|
247
|
-
.filters((f) => f.allowed('jobs.status', 'jobs.owner.region'))
|
|
248
|
-
.metric((m) =>
|
|
249
|
-
m
|
|
250
|
-
.aggregate('jobs.salary', 'avg')
|
|
251
|
-
.defaultAggregate('jobs.salary', 'avg'))
|
|
252
|
-
.chartType((t) => t.allowed('bar', 'line').default('bar'))
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
hiringModel.validateData({
|
|
256
|
-
jobs: jobsData,
|
|
257
|
-
owners: ownersData,
|
|
258
|
-
})
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
What the model can infer today:
|
|
262
|
-
|
|
263
|
-
- obvious one-hop lookup relationships from one dataset into another
|
|
264
|
-
- reusable shared-filter attributes backed by those relationships
|
|
265
|
-
- safe lookup-preserving model chart fields such as `jobs.owner.name`
|
|
266
|
-
- the base dataset for a model chart when every qualified field is anchored to
|
|
267
|
-
the same dataset id
|
|
268
|
-
|
|
269
|
-
How to prepare data for safe inference:
|
|
270
|
-
|
|
271
|
-
- declare one real key per dataset with `.key(...)`; single-column lookup keys
|
|
272
|
-
work best
|
|
273
|
-
- for the common case, use lookup datasets keyed by `id` and foreign keys named
|
|
274
|
-
`<singularDatasetId>Id` such as `ownerId`, `teacherId`, or `customerId`
|
|
275
|
-
- if a lookup key is already named `somethingId`, that same field name can be
|
|
276
|
-
inferred as a foreign key candidate on related datasets
|
|
277
|
-
- give lookup datasets a visible label-like column such as `name`, `title`, or
|
|
278
|
-
`label` when you want inferred shared filters to feel good by default
|
|
279
|
-
- leaving a raw field out of `.columns(...)` is not exclusion; use
|
|
280
|
-
`exclude(...)` only when you want that field removed from the chart contract
|
|
281
|
-
|
|
282
|
-
Important limits of the current model layer:
|
|
283
|
-
|
|
284
|
-
- inference is conservative; ambiguous candidates are ignored until you declare
|
|
285
|
-
`.relationship(...)` or suppress a false positive with
|
|
286
|
-
`.infer({ exclude: ['datasetId.columnId'] })`
|
|
287
|
-
- many-to-many stays explicit through `association(...)`
|
|
288
|
-
- model-aware charts allow one lookup hop only; they do not infer row-expanding
|
|
289
|
-
traversal
|
|
290
|
-
- if you use unqualified field ids, or fields anchored to multiple datasets,
|
|
291
|
-
add `.from('datasetId')`
|
|
292
|
-
- `validateData(...)` hard-fails on duplicate declared keys, orphan foreign keys, and malformed association edges
|
|
293
|
-
- charts still execute against one flat dataset at a time
|
|
294
|
-
- lookup-preserving model charts compile into the same explicit runtime core;
|
|
295
|
-
expanded chart grains still require `model.materialize(...)`
|
|
296
|
-
- explicit `.relationship(...)`, `.attribute(...)`, and `.association(...)`
|
|
297
|
-
remain available when inference is not enough
|
|
298
|
-
- linked metrics do not exist yet
|
|
299
|
-
|
|
300
|
-
### 4. Materialized views
|
|
301
|
-
|
|
302
|
-
Use `model.materialize(...)` when one chart truly needs a flat cross-dataset
|
|
303
|
-
analytic grain:
|
|
304
|
-
|
|
305
|
-
```tsx
|
|
306
|
-
const jobsWithOwner = hiringModel.materialize('jobsWithOwner', (m) =>
|
|
307
|
-
m
|
|
308
|
-
.from('jobs')
|
|
309
|
-
.join('owner', { relationship: 'jobs.ownerId -> owners.id' })
|
|
310
|
-
.grain('job')
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
const rows = jobsWithOwner.materialize({
|
|
314
|
-
jobs: jobsData,
|
|
315
|
-
owners: ownersData,
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
const chart = useChart({
|
|
319
|
-
data: rows,
|
|
320
|
-
schema: jobsWithOwner
|
|
321
|
-
.chart('jobsByOwner')
|
|
322
|
-
.xAxis((x) => x.allowed('ownerName').default('ownerName'))
|
|
323
|
-
.groupBy((g) => g.allowed('ownerRegion').default('ownerRegion'))
|
|
324
|
-
.metric((m) => m.aggregate('salary', 'sum').defaultAggregate('salary', 'sum')),
|
|
325
|
-
})
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
If you need a many-to-many chart grain such as `job-skill`, declare an explicit
|
|
329
|
-
`association(...)` on the model and then expand through it with
|
|
330
|
-
`.throughAssociation(...).grain(...)`.
|
|
331
|
-
|
|
332
|
-
Rules for materialized views:
|
|
333
|
-
|
|
334
|
-
- `materialize(...)` is explicit; the model does not become a hidden query engine
|
|
335
|
-
- prefer `model.chart(...)` for safe lookup-preserving fields; reach for `materialize(...)` when the chart grain actually changes
|
|
336
|
-
- `grain(...)` is required so the output row grain stays visible
|
|
337
|
-
- `.join(...)` is for lookup-style joins that preserve the base grain
|
|
338
|
-
- `.throughRelationship(...)` and `.throughAssociation(...)` are the explicit row-expanding paths
|
|
339
|
-
- many-to-many flattening stays visible because `association(...)` and `throughAssociation(...)` are both opt-in
|
|
340
|
-
- related-table columns reuse the linked dataset definitions, so you do not need repeated per-dataset derived columns like `ownerName`, `ownerRegion`, or `skillName`
|
|
341
|
-
- the materialized view is chartable like a normal dataset and exposes `materialize(data)` for explicit reuse and caching
|
|
342
|
-
|
|
343
|
-
### 5. Dashboard composition
|
|
344
|
-
|
|
345
|
-
Use `defineDashboard(model)` when several reusable charts belong to one
|
|
346
|
-
dashboard:
|
|
347
|
-
|
|
348
|
-
```tsx
|
|
349
|
-
import {
|
|
350
|
-
DashboardProvider,
|
|
351
|
-
defineDashboard,
|
|
352
|
-
useDashboard,
|
|
353
|
-
useDashboardChart,
|
|
354
|
-
} from '@matthieumordrel/chart-studio'
|
|
355
|
-
|
|
356
|
-
const hiringDashboard = defineDashboard(hiringModel)
|
|
357
|
-
.chart('jobsByOwner', jobsByOwner)
|
|
358
|
-
.sharedFilter('owner')
|
|
359
|
-
.build()
|
|
360
|
-
|
|
361
|
-
function HiringOverview() {
|
|
362
|
-
const dashboard = useDashboard({
|
|
363
|
-
definition: hiringDashboard,
|
|
364
|
-
data: {
|
|
365
|
-
jobs: jobsData,
|
|
366
|
-
owners: ownersData,
|
|
367
|
-
},
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
return (
|
|
371
|
-
<DashboardProvider dashboard={dashboard}>
|
|
372
|
-
<HiringChart />
|
|
373
|
-
</DashboardProvider>
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function HiringChart() {
|
|
378
|
-
const jobsChart = useDashboardChart(hiringDashboard, 'jobsByOwner')
|
|
379
|
-
|
|
380
|
-
return (
|
|
381
|
-
<Chart chart={jobsChart}>
|
|
382
|
-
<ChartCanvas />
|
|
383
|
-
</Chart>
|
|
384
|
-
)
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
Rules for dashboard composition:
|
|
389
|
-
|
|
390
|
-
- `defineDashboard(model)` is intentionally thin: chart registration, shared-filter selection, and optional dashboard-local shared filters
|
|
391
|
-
- dashboard charts may come from `dataset.chart(...)`, `model.chart(...)`, or `model.materialize(...).chart(...)`
|
|
392
|
-
- chart registration is explicit by id
|
|
393
|
-
- `useDashboard(...)` is the runtime boundary; it resolves model-aware charts and explicit materialized views against real data
|
|
394
|
-
- pass the explicit dashboard runtime into dashboard hooks, or inside a matching `DashboardProvider` pass the dashboard definition
|
|
395
|
-
- `useDashboardChart(...)` resolves the reusable chart by id and keeps React in charge of placement
|
|
396
|
-
- `useDashboardDataset(...)` exposes the globally filtered rows for non-chart consumers like KPI cards or tables
|
|
397
|
-
|
|
398
|
-
### 6. Shared dashboard filters
|
|
399
|
-
|
|
400
|
-
Shared dashboard filters layer on top of dashboard composition.
|
|
401
|
-
|
|
402
|
-
Reuse model-level relationship semantics when the same filter concept should
|
|
403
|
-
work across several dashboards:
|
|
404
|
-
|
|
405
|
-
```tsx
|
|
406
|
-
const dashboard = defineDashboard(hiringModel)
|
|
407
|
-
.chart('jobsByOwner', jobsByOwner)
|
|
408
|
-
.sharedFilter('owner')
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
Add one-off dashboard-local filters when the concept is specific to one
|
|
412
|
-
dashboard:
|
|
413
|
-
|
|
414
|
-
```tsx
|
|
415
|
-
const dashboard = defineDashboard(hiringModel)
|
|
416
|
-
.chart('jobsByOwner', jobsByOwner)
|
|
417
|
-
.sharedFilter('status', {
|
|
418
|
-
kind: 'select',
|
|
419
|
-
source: { dataset: 'jobs', column: 'status' },
|
|
420
|
-
})
|
|
421
|
-
.sharedFilter('activityDate', {
|
|
422
|
-
kind: 'date-range',
|
|
423
|
-
targets: [
|
|
424
|
-
{ dataset: 'jobs', column: 'createdAt' },
|
|
425
|
-
],
|
|
426
|
-
})
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
Rules for shared dashboard filters:
|
|
430
|
-
|
|
431
|
-
- `sharedFilter('owner')` can reuse either an inferred model attribute or an explicit one
|
|
432
|
-
- the model may infer useful attributes, but the dashboard still decides which ones become visible shared filters
|
|
433
|
-
- shared filters are explicit; nothing is guessed from chart configs
|
|
434
|
-
- shared filters narrow dataset slices before chart-local `useChart(...)` filters run
|
|
435
|
-
- local and global filters compose by intersection
|
|
436
|
-
- when a shared filter targets the same chart-local column, the dashboard owns that filter for that chart by default
|
|
437
|
-
- cross-dataset ambiguity requires an explicit model `attribute(...)` or explicit target choice
|
|
438
|
-
|
|
439
|
-
## Column Types
|
|
440
|
-
|
|
441
|
-
| Type | What it is for |
|
|
442
|
-
| ---------- | --------------------------------------- |
|
|
443
|
-
| `date` | time-series X-axis |
|
|
444
|
-
| `category` | categorical X-axis, grouping, filtering |
|
|
445
|
-
| `boolean` | grouping, filtering |
|
|
446
|
-
| `number` | metrics such as sum, avg, min, max |
|
|
447
|
-
|
|
448
|
-
## Declarative Schema and Control Restrictions
|
|
449
|
-
|
|
450
|
-
If you want to expose only a subset of groupings, metrics, chart types, or axes, use the fluent `defineDataset<Row>().chart(...)` builder:
|
|
451
|
-
|
|
452
|
-
```tsx
|
|
453
|
-
import { defineDataset, useChart } from '@matthieumordrel/chart-studio'
|
|
454
|
-
|
|
455
|
-
type Row = { periodEnd: string; segment: string; revenue: number; netIncome: number }
|
|
456
|
-
|
|
457
|
-
const schema = defineDataset<Row>()
|
|
458
|
-
.columns((c) => [
|
|
459
|
-
c.date('periodEnd', { label: 'Period End' }),
|
|
460
|
-
c.category('segment'),
|
|
461
|
-
c.number('revenue'),
|
|
462
|
-
c.number('netIncome')
|
|
463
|
-
])
|
|
464
|
-
.chart()
|
|
465
|
-
.xAxis((x) => x.allowed('periodEnd'))
|
|
466
|
-
.groupBy((g) => g.allowed('segment'))
|
|
467
|
-
.metric((m) =>
|
|
468
|
-
m
|
|
469
|
-
.count()
|
|
470
|
-
.aggregate('revenue', 'sum', 'avg')
|
|
471
|
-
.aggregate('netIncome', 'sum')
|
|
472
|
-
)
|
|
473
|
-
.chartType((t) => t.allowed('bar', 'line'))
|
|
474
|
-
.timeBucket((tb) => tb.allowed('year', 'quarter', 'month'))
|
|
475
|
-
|
|
476
|
-
const chart = useChart({ data, schema })
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
Why this pattern:
|
|
480
|
-
|
|
481
|
-
- `columns` defines types, labels, and formats for raw fields; use `c.exclude(...)` to remove a column from the chart
|
|
482
|
-
- Derived columns use `c.derived.*(...)` helpers for computed values from each row
|
|
483
|
-
- `xAxis`, `groupBy`, `metric`, `chartType`, and `timeBucket` restrict the allowed options
|
|
484
|
-
- invalid column IDs and config keys are rejected at compile time
|
|
485
|
-
- metric restrictions preserve the order you declare, so the first allowed metric becomes the default
|
|
486
|
-
|
|
487
|
-
## Headless Example
|
|
488
|
-
|
|
489
|
-
If you want to render your own UI or your own charting library, use only the core state:
|
|
490
|
-
|
|
491
|
-
```tsx
|
|
492
|
-
import { defineDataset, useChart } from '@matthieumordrel/chart-studio'
|
|
493
|
-
|
|
494
|
-
type Job = {
|
|
495
|
-
dateAdded: string
|
|
496
|
-
ownerName: string
|
|
497
|
-
salary: number
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const jobSchema = defineDataset<Job>()
|
|
501
|
-
.columns((c) => [
|
|
502
|
-
c.date('dateAdded', { label: 'Date Added' }),
|
|
503
|
-
c.category('ownerName', { label: 'Consultant' }),
|
|
504
|
-
c.number('salary', { label: 'Salary' })
|
|
505
|
-
])
|
|
506
|
-
.chart()
|
|
507
|
-
|
|
508
|
-
export function JobsChartHeadless({ data }: { data: Job[] }) {
|
|
509
|
-
const chart = useChart({ data, schema: jobSchema })
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<div>
|
|
513
|
-
<div>Chart type: {chart.chartType}</div>
|
|
514
|
-
<div>Rows: {chart.transformedData.length}</div>
|
|
515
|
-
<pre>{JSON.stringify(chart.transformedData, null, 2)}</pre>
|
|
516
|
-
</div>
|
|
517
|
-
)
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Styling Requirements
|
|
522
|
-
|
|
523
|
-
The headless core has no styling requirements.
|
|
524
|
-
|
|
525
|
-
The `ui` layer is Tailwind-based and uses semantic classes such as:
|
|
526
|
-
|
|
527
|
-
- `bg-background`
|
|
528
|
-
- `text-foreground`
|
|
529
|
-
- `border-border`
|
|
530
|
-
- `bg-popover`
|
|
531
|
-
- `text-muted-foreground`
|
|
532
|
-
|
|
533
|
-
For those classes to render correctly, Tailwind needs real values behind tokens like `background`, `foreground`, `border`, and `popover`.
|
|
534
|
-
|
|
535
|
-
You can use `ui` in two ways:
|
|
536
|
-
|
|
537
|
-
### 1. Recommended: import the built-in theme
|
|
538
|
-
|
|
539
|
-
This is the easiest setup:
|
|
540
|
-
|
|
541
|
-
```css
|
|
542
|
-
@import 'tailwindcss';
|
|
543
|
-
@import '@matthieumordrel/chart-studio/ui/theme.css';
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
This does three things for you:
|
|
547
|
-
|
|
548
|
-
- Tailwind utilities for the package components
|
|
549
|
-
- automatic scanning of the package UI classes
|
|
550
|
-
- default fallback values for all semantic UI tokens
|
|
551
|
-
- built-in light and dark default themes
|
|
552
|
-
|
|
553
|
-
If your app already defines matching shadcn-style variables, those values take over automatically. If not, the built-in defaults are used.
|
|
554
|
-
|
|
555
|
-
The shipped theme supports dark mode through either:
|
|
556
|
-
|
|
557
|
-
- `.dark`
|
|
558
|
-
- `[data-theme="dark"]`
|
|
559
|
-
|
|
560
|
-
### 2. Advanced: define everything yourself
|
|
561
|
-
|
|
562
|
-
If you do not want to import `@matthieumordrel/chart-studio/ui/theme.css`, you can provide all the required semantic tokens yourself in your app theme.
|
|
563
|
-
|
|
564
|
-
If neither of those is true, use the headless core and render your own controls.
|
|
565
|
-
|
|
566
|
-
### Minimum UI theme contract
|
|
567
|
-
|
|
568
|
-
You do not need shadcn itself to use `@matthieumordrel/chart-studio/ui`.
|
|
569
|
-
|
|
570
|
-
If you import `@matthieumordrel/chart-studio/ui/theme.css`, every token below gets a built-in fallback automatically.
|
|
571
|
-
|
|
572
|
-
If your app already defines some of these variables, your values override the defaults for those specific tokens only. Missing ones still fall back to the package defaults.
|
|
573
|
-
|
|
574
|
-
These are the tokens currently expected by the UI layer:
|
|
575
|
-
|
|
576
|
-
| Token | Purpose |
|
|
577
|
-
| -------------------- | -------------------------------------- |
|
|
578
|
-
| `background` | control backgrounds and input surfaces |
|
|
579
|
-
| `foreground` | primary text |
|
|
580
|
-
| `muted` | subtle backgrounds and hover states |
|
|
581
|
-
| `muted-foreground` | secondary text and icons |
|
|
582
|
-
| `border` | outlines and separators |
|
|
583
|
-
| `popover` | dropdowns and floating panels |
|
|
584
|
-
| `popover-foreground` | popover text color |
|
|
585
|
-
| `primary` | selected and active states |
|
|
586
|
-
| `primary-foreground` | text on filled primary surfaces |
|
|
587
|
-
| `ring` | focus-visible ring color |
|
|
588
|
-
|
|
589
|
-
Minimal example:
|
|
590
|
-
|
|
591
|
-
```css
|
|
592
|
-
:root {
|
|
593
|
-
--background: 0 0% 100%;
|
|
594
|
-
--foreground: 222.2 84% 4.9%;
|
|
595
|
-
--muted: 210 40% 96.1%;
|
|
596
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
597
|
-
--border: 214.3 31.8% 91.4%;
|
|
598
|
-
--popover: 0 0% 100%;
|
|
599
|
-
--popover-foreground: 222.2 84% 4.9%;
|
|
600
|
-
--primary: 222.2 47.4% 11.2%;
|
|
601
|
-
--primary-foreground: 210 40% 98%;
|
|
602
|
-
--ring: 221.2 83.2% 53.3%;
|
|
603
|
-
}
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
How this works in practice:
|
|
607
|
-
|
|
608
|
-
- import `ui/theme.css` and do nothing else: the package uses its own defaults
|
|
609
|
-
- toggle dark mode with either `.dark` or `[data-theme="dark"]`: the package uses its built-in dark defaults
|
|
610
|
-
- import `ui/theme.css` and define only a few variables: your values win for those variables, defaults cover the rest
|
|
611
|
-
- skip `ui/theme.css`: you must define the whole token contract yourself
|
|
612
|
-
|
|
613
|
-
That makes the package usable out of the box while still being easy to theme.
|
|
614
|
-
|
|
615
|
-
### Optional chart color tokens
|
|
616
|
-
|
|
617
|
-
Chart series colors also support shadcn-style chart variables:
|
|
618
|
-
|
|
619
|
-
| Token | Purpose |
|
|
620
|
-
| --------- | ------------------- |
|
|
621
|
-
| `chart-1` | first series color |
|
|
622
|
-
| `chart-2` | second series color |
|
|
623
|
-
| `chart-3` | third series color |
|
|
624
|
-
| `chart-4` | fourth series color |
|
|
625
|
-
| `chart-5` | fifth series color |
|
|
626
|
-
|
|
627
|
-
These are also optional when you import `ui/theme.css`.
|
|
628
|
-
|
|
629
|
-
If your app defines `--chart-1` through `--chart-5`, those colors are used automatically.
|
|
630
|
-
|
|
631
|
-
If they are not defined, `chart-studio` falls back to a built-in OKLCH palette, with separate light and dark defaults. That is why you may see blue, rose, cyan, or other fallback colors in charts when your app does not provide chart variables.
|
|
632
|
-
|
|
633
|
-
Minimal example:
|
|
634
|
-
|
|
635
|
-
```css
|
|
636
|
-
:root {
|
|
637
|
-
--chart-1: 221.2 83.2% 53.3%;
|
|
638
|
-
--chart-2: 262.1 83.3% 57.8%;
|
|
639
|
-
--chart-3: 24.6 95% 53.1%;
|
|
640
|
-
--chart-4: 142.1 76.2% 36.3%;
|
|
641
|
-
--chart-5: 346.8 77.2% 49.8%;
|
|
642
|
-
}
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
## Common Questions
|
|
646
|
-
|
|
647
|
-
### Which import path should I use?
|
|
648
|
-
|
|
649
|
-
- Use `@matthieumordrel/chart-studio` for the headless core.
|
|
650
|
-
- Use `@matthieumordrel/chart-studio/ui` for the optional UI components.
|
|
651
|
-
|
|
652
|
-
### Do I need Recharts?
|
|
653
|
-
|
|
654
|
-
Only for the UI layer. The headless core works without it.
|
|
655
|
-
|
|
656
|
-
### Do I need Tailwind?
|
|
657
|
-
|
|
658
|
-
Only for the UI layer. The headless core does not require it.
|
|
659
|
-
|
|
660
|
-
### Can I use multiple datasets?
|
|
661
|
-
|
|
662
|
-
Yes, but there are two different meanings:
|
|
663
|
-
|
|
664
|
-
- independent datasets: use separate `useChart(...)` calls, or `useChart({ sources: [...] })` when one chart should switch between flat sources
|
|
665
|
-
- related datasets: use `defineDataModel().dataset(...).infer(...)`, then `model.chart(...)` for safe lookup-preserving charts
|
|
666
|
-
- explicit expanded grains: use `model.materialize(...)`
|
|
667
|
-
- screen-level composition and shared state: use `defineDashboard(model)` plus `useDashboard(...)`
|
|
668
|
-
|
|
669
|
-
The current chart runtime still executes one flat dataset at a time. Multi-source
|
|
670
|
-
source-switching is separate from linked data models, explicit materialized
|
|
671
|
-
views, and dashboard composition.
|
|
672
|
-
|
|
673
|
-
If you are starting fresh with a dashboard, use the model-first path:
|
|
674
|
-
`defineDataModel(...)`, `model.chart(...)` / `model.materialize(...)`,
|
|
675
|
-
`defineDashboard(...)`, and `useDashboard(...)`.
|
|
676
|
-
|
|
677
|
-
```tsx
|
|
678
|
-
import { defineDataset, useChart } from '@matthieumordrel/chart-studio'
|
|
679
|
-
|
|
680
|
-
const chart = useChart({
|
|
681
|
-
sources: [
|
|
682
|
-
{
|
|
683
|
-
id: 'jobs',
|
|
684
|
-
label: 'Jobs',
|
|
685
|
-
data: jobs,
|
|
686
|
-
schema: defineDataset<Job>()
|
|
687
|
-
.columns((c) => [c.date('dateAdded', { label: 'Date Added' })])
|
|
688
|
-
.chart()
|
|
689
|
-
},
|
|
690
|
-
{ id: 'candidates', label: 'Candidates', data: candidates }
|
|
691
|
-
]
|
|
692
|
-
})
|
|
20
|
+
import {defineDataset, useChart} from '@matthieumordrel/chart-studio'
|
|
693
21
|
```
|
|
694
22
|
|
|
695
|
-
|
|
696
|
-
active source at a time, so this is not dashboard composition and not
|
|
697
|
-
cross-dataset execution.
|
|
698
|
-
|
|
699
|
-
### Can outside state drive one chart's filters or date range?
|
|
700
|
-
|
|
701
|
-
Yes. Use `inputs` for externally controlled data-scope state:
|
|
702
|
-
|
|
703
|
-
```tsx
|
|
704
|
-
const chart = useChart({
|
|
705
|
-
data: jobs,
|
|
706
|
-
schema,
|
|
707
|
-
inputs: {
|
|
708
|
-
filters,
|
|
709
|
-
onFiltersChange: setFilters,
|
|
710
|
-
referenceDateId,
|
|
711
|
-
onReferenceDateIdChange: setReferenceDateId,
|
|
712
|
-
dateRange,
|
|
713
|
-
onDateRangeChange: setDateRange
|
|
714
|
-
}
|
|
715
|
-
})
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
Rules:
|
|
719
|
-
|
|
720
|
-
- `inputs` only covers data-scope state: filters, reference date, and date range
|
|
721
|
-
- presentation controls such as `xAxis`, `groupBy`, `metric`, and `chartType` stay chart-local
|
|
722
|
-
- `dateRange` is `{ preset, customFilter }`
|
|
723
|
-
- when an input is controlled, chart setters request changes through the matching callback
|
|
724
|
-
- `chart.filters` and related date state are always the sanitized effective state for the active source
|
|
725
|
-
- this still does not create a dashboard runtime or shared state between charts; use `defineDashboard()` for that
|
|
726
|
-
|
|
727
|
-
### What chart types are available?
|
|
728
|
-
|
|
729
|
-
- date X-axis: `bar`, `line`, `area`
|
|
730
|
-
- category or boolean X-axis: `bar`, `pie`, `donut`
|
|
731
|
-
- `pie` and `donut` do not support `groupBy`
|
|
732
|
-
|
|
733
|
-
## Troubleshooting
|
|
734
|
-
|
|
735
|
-
### The UI looks mostly unstyled
|
|
736
|
-
|
|
737
|
-
If the components render but look plain, compressed, or layout incorrectly, the most common cause is that the package theme file is not imported.
|
|
738
|
-
|
|
739
|
-
Start with:
|
|
740
|
-
|
|
741
|
-
```css
|
|
742
|
-
@import 'tailwindcss';
|
|
743
|
-
@import '@matthieumordrel/chart-studio/ui/theme.css';
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
If you are importing the package source directly in a local playground or monorepo, make sure Tailwind is scanning those source files too.
|
|
747
|
-
|
|
748
|
-
If your app already uses shadcn-style tokens, also make sure tokens such as `background`, `foreground`, `muted`, `border`, `popover`, `primary`, `ring`, and optionally `chart-1` through `chart-5` are defined in your theme.
|
|
749
|
-
|
|
750
|
-
## On the Radar
|
|
751
|
-
|
|
752
|
-
These are known limitations and areas being considered for future versions. None of these are committed — they represent directions the library may grow based on real usage.
|
|
753
|
-
|
|
754
|
-
### Renderer flexibility
|
|
755
|
-
|
|
756
|
-
The UI layer currently only supports Recharts. If you want to use ECharts, Plotly, or another renderer, you can use the headless core but lose the built-in toolbar and canvas composition. A renderer adapter pattern for `<ChartCanvas>` could make the UI layer renderer-agnostic.
|
|
757
|
-
|
|
758
|
-
### Richer aggregation
|
|
759
|
-
|
|
760
|
-
The pipeline supports sum, avg, min, and max. Derived columns can access multiple fields of a single row (e.g. `row.revenue - row.cost`), but there is no support yet for metrics that depend on other rows or on aggregated results — things like "% of total", running totals, percentiles, or post-aggregation ratios (e.g. total revenue / total orders).
|
|
761
|
-
|
|
762
|
-
### Chart interactivity
|
|
763
|
-
|
|
764
|
-
There is currently no built-in support for drill-down, click-to-filter, brush selection, or linked charts. The headless state can be wired manually to achieve some of these, but first-class interactivity primitives would make this significantly easier.
|
|
765
|
-
|
|
766
|
-
### Multi-dataset composition
|
|
767
|
-
|
|
768
|
-
Dashboard composition and shared dashboard filters are now available, but each
|
|
769
|
-
chart instance still operates on one flat dataset at a time. Overlaying series
|
|
770
|
-
from different schemas (e.g. revenue on the left Y-axis and headcount on the
|
|
771
|
-
right) would require separate chart instances today. Dual-axis cross-dataset
|
|
772
|
-
execution, automatic denormalization, and linked metrics are not yet supported.
|
|
773
|
-
|
|
774
|
-
### Schema Builder Ergonomics
|
|
775
|
-
|
|
776
|
-
`defineDataset<Row>()` owns the reusable `.columns(...)` contract, and
|
|
777
|
-
`defineDataset<Row>().chart(...)` is the single explicit path for declaring a
|
|
778
|
-
chart schema. Both feed the same chart-definition surface that you pass directly
|
|
779
|
-
to `useChart(...)` or `inferColumnsFromData(...)`.
|
|
23
|
+
If you also want the optional ready-made React UI, install `@matthieumordrel/chart-studio-ui` alongside this package.
|
|
780
24
|
|
|
781
|
-
|
|
25
|
+
Special thanks to the teams behind TanStack Table and Recharts.
|
|
782
26
|
|
|
783
|
-
|
|
784
|
-
- `bun run release:publish -- --tag=latest`
|
|
785
|
-
- `npm publish` runs `prepublishOnly`, which calls `bun run release:check`
|
|
27
|
+
Full documentation: <https://github.com/MatthieuMordrel/chart-studio#readme>
|