@matthieumordrel/chart-studio 0.4.0 → 0.5.2

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 (66) hide show
  1. package/README.md +10 -768
  2. package/dist/_internal.d.mts +9 -0
  3. package/dist/_internal.mjs +9 -0
  4. package/dist/core/chart-capabilities.d.mts +5 -0
  5. package/dist/core/chart-capabilities.mjs +9 -0
  6. package/dist/core/config-utils.mjs +2 -1
  7. package/dist/core/data-label-defaults.d.mts +92 -0
  8. package/dist/core/data-label-defaults.mjs +4 -0
  9. package/dist/core/date-range-presets.d.mts +43 -1
  10. package/dist/core/date-range-presets.mjs +1 -1
  11. package/dist/core/date-utils.d.mts +26 -0
  12. package/dist/core/formatting.d.mts +49 -0
  13. package/dist/core/formatting.mjs +32 -10
  14. package/dist/core/index.d.mts +5 -3
  15. package/dist/core/metric-utils.d.mts +18 -2
  16. package/dist/core/metric-utils.mjs +1 -1
  17. package/dist/core/types.d.mts +2 -2
  18. package/dist/core/use-chart.mjs +1 -1
  19. package/dist/core/use-dashboard.mjs +1 -1
  20. package/dist/index.d.mts +7 -4
  21. package/dist/index.mjs +4 -1
  22. package/package.json +10 -41
  23. package/LICENSE +0 -21
  24. package/dist/ui/chart-axis-ticks.mjs +0 -65
  25. package/dist/ui/chart-canvas.d.mts +0 -40
  26. package/dist/ui/chart-canvas.mjs +0 -790
  27. package/dist/ui/chart-context.d.mts +0 -101
  28. package/dist/ui/chart-context.mjs +0 -117
  29. package/dist/ui/chart-date-range-badge.d.mts +0 -20
  30. package/dist/ui/chart-date-range-badge.mjs +0 -49
  31. package/dist/ui/chart-date-range-panel.d.mts +0 -18
  32. package/dist/ui/chart-date-range-panel.mjs +0 -126
  33. package/dist/ui/chart-date-range.d.mts +0 -20
  34. package/dist/ui/chart-date-range.mjs +0 -67
  35. package/dist/ui/chart-debug.d.mts +0 -21
  36. package/dist/ui/chart-debug.mjs +0 -173
  37. package/dist/ui/chart-dropdown.mjs +0 -92
  38. package/dist/ui/chart-filters-panel.d.mts +0 -26
  39. package/dist/ui/chart-filters-panel.mjs +0 -258
  40. package/dist/ui/chart-filters.d.mts +0 -18
  41. package/dist/ui/chart-filters.mjs +0 -48
  42. package/dist/ui/chart-group-by-selector.d.mts +0 -16
  43. package/dist/ui/chart-group-by-selector.mjs +0 -32
  44. package/dist/ui/chart-metric-panel.d.mts +0 -25
  45. package/dist/ui/chart-metric-panel.mjs +0 -172
  46. package/dist/ui/chart-metric-selector.d.mts +0 -16
  47. package/dist/ui/chart-metric-selector.mjs +0 -50
  48. package/dist/ui/chart-select.mjs +0 -61
  49. package/dist/ui/chart-source-switcher.d.mts +0 -24
  50. package/dist/ui/chart-source-switcher.mjs +0 -56
  51. package/dist/ui/chart-time-bucket-selector.d.mts +0 -17
  52. package/dist/ui/chart-time-bucket-selector.mjs +0 -37
  53. package/dist/ui/chart-toolbar-overflow.d.mts +0 -28
  54. package/dist/ui/chart-toolbar-overflow.mjs +0 -223
  55. package/dist/ui/chart-toolbar.d.mts +0 -33
  56. package/dist/ui/chart-toolbar.mjs +0 -60
  57. package/dist/ui/chart-type-selector.d.mts +0 -19
  58. package/dist/ui/chart-type-selector.mjs +0 -168
  59. package/dist/ui/chart-x-axis-selector.d.mts +0 -16
  60. package/dist/ui/chart-x-axis-selector.mjs +0 -28
  61. package/dist/ui/index.d.mts +0 -19
  62. package/dist/ui/index.mjs +0 -18
  63. package/dist/ui/percent-stacked.mjs +0 -36
  64. package/dist/ui/theme.css +0 -67
  65. package/dist/ui/toolbar-types.d.mts +0 -7
  66. 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
- Composable charting for React with two adoption paths:
3
+ > Early alpha. Active work in progress. Not recommended for production use yet.
4
4
 
5
- - use the **headless core** if you want chart state, filtering, grouping, and transformed data
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
- Package: `@matthieumordrel/chart-studio`
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 recharts lucide-react
12
+ bun add @matthieumordrel/chart-studio@alpha react
63
13
  ```
64
14
 
65
- Then import the package theme once in your app stylesheet:
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 } from '@matthieumordrel/chart-studio'
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
- Each source may use `defineDataset<Row>().chart(...)`. The chart still reads one
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
- ## Release
25
+ Special thanks to the teams behind TanStack Table and Recharts.
782
26
 
783
- - `bun run release:check`
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>