@matthieumordrel/chart-studio 0.2.3 → 0.2.5
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 +42 -42
- package/dist/core/chart-capabilities.d.mts +15 -0
- package/dist/core/chart-capabilities.mjs +23 -0
- package/dist/core/colors.mjs +5 -5
- package/dist/core/config-utils.mjs +6 -2
- package/dist/core/date-range-presets.d.mts +12 -0
- package/dist/core/date-range-presets.mjs +152 -0
- package/dist/core/define-chart-schema.d.mts +26 -94
- package/dist/core/define-chart-schema.mjs +26 -34
- package/dist/core/infer-columns.d.mts +2 -2
- package/dist/core/infer-columns.mjs +4 -2
- package/dist/core/metric-utils.mjs +13 -5
- package/dist/core/pipeline-data-points.mjs +4 -1
- package/dist/core/schema-builder.mjs +335 -0
- package/dist/core/schema-builder.types.d.mts +279 -0
- package/dist/core/types.d.mts +61 -17
- package/dist/core/use-chart-options.d.mts +7 -4
- package/dist/core/use-chart.d.mts +4 -4
- package/dist/core/use-chart.mjs +70 -40
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/ui/chart-canvas.d.mts +8 -4
- package/dist/ui/chart-canvas.mjs +347 -29
- package/dist/ui/chart-context.d.mts +11 -4
- package/dist/ui/chart-context.mjs +3 -0
- package/dist/ui/chart-date-range-badge.mjs +2 -2
- package/dist/ui/chart-date-range-panel.mjs +19 -101
- package/dist/ui/chart-date-range.mjs +3 -3
- package/dist/ui/chart-debug.d.mts +6 -2
- package/dist/ui/chart-debug.mjs +5 -1
- package/dist/ui/chart-group-by-selector.d.mts +3 -1
- package/dist/ui/chart-group-by-selector.mjs +4 -1
- package/dist/ui/chart-metric-selector.mjs +2 -2
- package/dist/ui/chart-select.mjs +9 -10
- package/dist/ui/chart-source-switcher.d.mts +3 -1
- package/dist/ui/chart-source-switcher.mjs +4 -2
- package/dist/ui/chart-time-bucket-selector.d.mts +3 -1
- package/dist/ui/chart-time-bucket-selector.mjs +4 -1
- package/dist/ui/chart-toolbar-overflow.mjs +48 -26
- package/dist/ui/chart-toolbar.d.mts +6 -2
- package/dist/ui/chart-toolbar.mjs +4 -0
- package/dist/ui/chart-type-selector.d.mts +7 -2
- package/dist/ui/chart-type-selector.mjs +155 -20
- package/dist/ui/chart-x-axis-selector.d.mts +3 -1
- package/dist/ui/chart-x-axis-selector.mjs +4 -1
- package/dist/ui/percent-stacked.mjs +36 -0
- package/dist/ui/theme.css +54 -49
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ You get:
|
|
|
22
22
|
- transformed chart data
|
|
23
23
|
- filtering, grouping, metrics, and time bucketing logic
|
|
24
24
|
|
|
25
|
+
Requirements:
|
|
26
|
+
|
|
27
|
+
- `react` >= 18.2.0
|
|
28
|
+
|
|
25
29
|
Install:
|
|
26
30
|
|
|
27
31
|
```bash
|
|
@@ -46,6 +50,12 @@ You get:
|
|
|
46
50
|
- `<ChartCanvas>`
|
|
47
51
|
- granular UI controls from `@matthieumordrel/chart-studio/ui`
|
|
48
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)
|
|
58
|
+
|
|
49
59
|
Install:
|
|
50
60
|
|
|
51
61
|
```bash
|
|
@@ -88,7 +98,7 @@ export function JobsChart() {
|
|
|
88
98
|
## How It Works
|
|
89
99
|
|
|
90
100
|
1. Pass your raw data to `useChart()`.
|
|
91
|
-
2. Add an optional `schema` with `defineChartSchema<Row>()
|
|
101
|
+
2. Add an optional `schema` with `defineChartSchema<Row>()...` when you need labels, type overrides, derived columns, or control restrictions (allowed metrics, groupings, chart types, etc.).
|
|
92
102
|
3. Either render your own UI from the returned state, or use the components from `@matthieumordrel/chart-studio/ui`.
|
|
93
103
|
|
|
94
104
|
## Column Types
|
|
@@ -102,41 +112,39 @@ export function JobsChart() {
|
|
|
102
112
|
|
|
103
113
|
## Declarative Schema and Control Restrictions
|
|
104
114
|
|
|
105
|
-
If you want to expose only a subset of groupings, metrics, chart types, or axes, use `defineChartSchema<Row>()
|
|
115
|
+
If you want to expose only a subset of groupings, metrics, chart types, or axes, use the fluent `defineChartSchema<Row>()` builder:
|
|
106
116
|
|
|
107
117
|
```tsx
|
|
108
118
|
import { defineChartSchema, useChart } from '@matthieumordrel/chart-studio'
|
|
109
119
|
|
|
110
120
|
type Row = { periodEnd: string; segment: string; revenue: number; netIncome: number }
|
|
111
121
|
|
|
112
|
-
const schema = defineChartSchema<Row>()
|
|
113
|
-
columns
|
|
114
|
-
periodEnd
|
|
115
|
-
segment
|
|
116
|
-
revenue
|
|
117
|
-
netIncome
|
|
118
|
-
|
|
119
|
-
xAxis
|
|
120
|
-
groupBy
|
|
121
|
-
metric
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
timeBucket: { allowed: ['year', 'quarter', 'month'] }
|
|
130
|
-
})
|
|
122
|
+
const schema = defineChartSchema<Row>()
|
|
123
|
+
.columns((c) => [
|
|
124
|
+
c.date('periodEnd', { label: 'Period End' }),
|
|
125
|
+
c.category('segment'),
|
|
126
|
+
c.number('revenue'),
|
|
127
|
+
c.number('netIncome')
|
|
128
|
+
])
|
|
129
|
+
.xAxis((x) => x.allowed('periodEnd'))
|
|
130
|
+
.groupBy((g) => g.allowed('segment'))
|
|
131
|
+
.metric((m) =>
|
|
132
|
+
m
|
|
133
|
+
.count()
|
|
134
|
+
.aggregate('revenue', 'sum', 'avg')
|
|
135
|
+
.aggregate('netIncome', 'sum')
|
|
136
|
+
)
|
|
137
|
+
.chartType((t) => t.allowed('bar', 'line'))
|
|
138
|
+
.timeBucket((tb) => tb.allowed('year', 'quarter', 'month'))
|
|
131
139
|
|
|
132
140
|
const chart = useChart({ data, schema })
|
|
133
141
|
```
|
|
134
142
|
|
|
135
143
|
Why this pattern:
|
|
136
144
|
|
|
137
|
-
- `columns` defines types, labels, and formats for raw fields; use `
|
|
138
|
-
- Derived columns use `
|
|
139
|
-
- `xAxis`, `groupBy`, `metric`, `chartType`, `timeBucket` restrict the allowed options
|
|
145
|
+
- `columns` defines types, labels, and formats for raw fields; use `c.exclude(...)` to remove a column from the chart
|
|
146
|
+
- Derived columns use `c.derived.*(...)` helpers for computed values from each row
|
|
147
|
+
- `xAxis`, `groupBy`, `metric`, `chartType`, and `timeBucket` restrict the allowed options
|
|
140
148
|
- invalid column IDs and config keys are rejected at compile time
|
|
141
149
|
- metric restrictions preserve the order you declare, so the first allowed metric becomes the default
|
|
142
150
|
|
|
@@ -153,13 +161,12 @@ type Job = {
|
|
|
153
161
|
salary: number
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
const jobSchema = defineChartSchema<Job>()
|
|
157
|
-
columns
|
|
158
|
-
dateAdded
|
|
159
|
-
ownerName
|
|
160
|
-
salary
|
|
161
|
-
|
|
162
|
-
})
|
|
164
|
+
const jobSchema = defineChartSchema<Job>()
|
|
165
|
+
.columns((c) => [
|
|
166
|
+
c.date('dateAdded', { label: 'Date Added' }),
|
|
167
|
+
c.category('ownerName', { label: 'Consultant' }),
|
|
168
|
+
c.number('salary', { label: 'Salary' })
|
|
169
|
+
])
|
|
163
170
|
|
|
164
171
|
export function JobsChartHeadless({ data }: { data: Job[] }) {
|
|
165
172
|
const chart = useChart({ data, schema: jobSchema })
|
|
@@ -298,12 +305,6 @@ Minimal example:
|
|
|
298
305
|
}
|
|
299
306
|
```
|
|
300
307
|
|
|
301
|
-
## Compatibility
|
|
302
|
-
|
|
303
|
-
- `react`: `>=18.2.0 <20`
|
|
304
|
-
- `recharts`: `>=3.0.0 <4` for the UI layer
|
|
305
|
-
- `lucide-react`: `>=0.577.0 <1` for the UI layer
|
|
306
|
-
|
|
307
308
|
## Common Questions
|
|
308
309
|
|
|
309
310
|
### Which import path should I use?
|
|
@@ -332,9 +333,8 @@ const chart = useChart({
|
|
|
332
333
|
id: 'jobs',
|
|
333
334
|
label: 'Jobs',
|
|
334
335
|
data: jobs,
|
|
335
|
-
schema: defineChartSchema<Job>()
|
|
336
|
-
columns
|
|
337
|
-
})
|
|
336
|
+
schema: defineChartSchema<Job>()
|
|
337
|
+
.columns((c) => [c.date('dateAdded', { label: 'Date Added' })])
|
|
338
338
|
},
|
|
339
339
|
{ id: 'candidates', label: 'Candidates', data: candidates }
|
|
340
340
|
]
|
|
@@ -384,9 +384,9 @@ There is currently no built-in support for drill-down, click-to-filter, brush se
|
|
|
384
384
|
|
|
385
385
|
Each chart instance operates on a single flat dataset. Overlaying series from different schemas (e.g. revenue on the left Y-axis and headcount on the right) would require separate chart instances today. Dual-axis and cross-dataset composition are not yet supported.
|
|
386
386
|
|
|
387
|
-
###
|
|
387
|
+
### Schema Builder Ergonomics
|
|
388
388
|
|
|
389
|
-
`defineChartSchema<Row>()
|
|
389
|
+
`defineChartSchema<Row>()` now returns one fluent builder that you pass directly to `useChart(...)` or `inferColumnsFromData(...)`. That keeps the public API strongly typed while improving IntelliSense for raw field ids, derived columns, and control restrictions.
|
|
390
390
|
|
|
391
391
|
## Release
|
|
392
392
|
|
|
@@ -23,6 +23,16 @@ declare const CHART_TYPE_CONFIG: {
|
|
|
23
23
|
readonly supportsGrouping: true;
|
|
24
24
|
readonly supportsTimeBucketing: true;
|
|
25
25
|
};
|
|
26
|
+
readonly 'grouped-bar': {
|
|
27
|
+
readonly supportedXAxisTypes: readonly ["date", "category", "boolean"];
|
|
28
|
+
readonly supportsGrouping: true;
|
|
29
|
+
readonly supportsTimeBucketing: true;
|
|
30
|
+
};
|
|
31
|
+
readonly 'percent-bar': {
|
|
32
|
+
readonly supportedXAxisTypes: readonly ["date", "category", "boolean"];
|
|
33
|
+
readonly supportsGrouping: true;
|
|
34
|
+
readonly supportsTimeBucketing: true;
|
|
35
|
+
};
|
|
26
36
|
readonly line: {
|
|
27
37
|
readonly supportedXAxisTypes: readonly ["date"];
|
|
28
38
|
readonly supportsGrouping: true;
|
|
@@ -33,6 +43,11 @@ declare const CHART_TYPE_CONFIG: {
|
|
|
33
43
|
readonly supportsGrouping: true;
|
|
34
44
|
readonly supportsTimeBucketing: true;
|
|
35
45
|
};
|
|
46
|
+
readonly 'percent-area': {
|
|
47
|
+
readonly supportedXAxisTypes: readonly ["date"];
|
|
48
|
+
readonly supportsGrouping: true;
|
|
49
|
+
readonly supportsTimeBucketing: true;
|
|
50
|
+
};
|
|
36
51
|
readonly pie: {
|
|
37
52
|
readonly supportedXAxisTypes: readonly ["category", "boolean"];
|
|
38
53
|
readonly supportsGrouping: false;
|
|
@@ -13,6 +13,24 @@ const CHART_TYPE_CONFIG = {
|
|
|
13
13
|
supportsGrouping: true,
|
|
14
14
|
supportsTimeBucketing: true
|
|
15
15
|
},
|
|
16
|
+
"grouped-bar": {
|
|
17
|
+
supportedXAxisTypes: [
|
|
18
|
+
"date",
|
|
19
|
+
"category",
|
|
20
|
+
"boolean"
|
|
21
|
+
],
|
|
22
|
+
supportsGrouping: true,
|
|
23
|
+
supportsTimeBucketing: true
|
|
24
|
+
},
|
|
25
|
+
"percent-bar": {
|
|
26
|
+
supportedXAxisTypes: [
|
|
27
|
+
"date",
|
|
28
|
+
"category",
|
|
29
|
+
"boolean"
|
|
30
|
+
],
|
|
31
|
+
supportsGrouping: true,
|
|
32
|
+
supportsTimeBucketing: true
|
|
33
|
+
},
|
|
16
34
|
line: {
|
|
17
35
|
supportedXAxisTypes: ["date"],
|
|
18
36
|
supportsGrouping: true,
|
|
@@ -23,6 +41,11 @@ const CHART_TYPE_CONFIG = {
|
|
|
23
41
|
supportsGrouping: true,
|
|
24
42
|
supportsTimeBucketing: true
|
|
25
43
|
},
|
|
44
|
+
"percent-area": {
|
|
45
|
+
supportedXAxisTypes: ["date"],
|
|
46
|
+
supportsGrouping: true,
|
|
47
|
+
supportsTimeBucketing: true
|
|
48
|
+
},
|
|
26
49
|
pie: {
|
|
27
50
|
supportedXAxisTypes: ["category", "boolean"],
|
|
28
51
|
supportsGrouping: false,
|
package/dist/core/colors.mjs
CHANGED
|
@@ -20,11 +20,11 @@ const FALLBACK_COLORS = [
|
|
|
20
20
|
];
|
|
21
21
|
/** Shadcn chart CSS variables (5 colors) with safe fallbacks. */
|
|
22
22
|
const SHADCN_CHART_COLORS = [
|
|
23
|
-
`var(--chart-1, var(--cs-chart-1,
|
|
24
|
-
`var(--chart-2, var(--cs-chart-2,
|
|
25
|
-
`var(--chart-3, var(--cs-chart-3,
|
|
26
|
-
`var(--chart-4, var(--cs-chart-4,
|
|
27
|
-
`var(--chart-5, var(--cs-chart-5,
|
|
23
|
+
`var(--chart-1, var(--cs-chart-1, oklch(0.501 0.228 277.992)))`,
|
|
24
|
+
`var(--chart-2, var(--cs-chart-2, oklch(0.550 0.235 302.715)))`,
|
|
25
|
+
`var(--chart-3, var(--cs-chart-3, oklch(0.609 0.206 354.673)))`,
|
|
26
|
+
`var(--chart-4, var(--cs-chart-4, oklch(0.635 0.109 178.228)))`,
|
|
27
|
+
`var(--chart-5, var(--cs-chart-5, oklch(0.732 0.166 58.213)))`
|
|
28
28
|
];
|
|
29
29
|
/**
|
|
30
30
|
* Get a color for the Nth series.
|
|
@@ -50,9 +50,10 @@ function restrictConfiguredValues(values, config, fallbackToBaseIfEmpty = false)
|
|
|
50
50
|
/**
|
|
51
51
|
* Resolve one primitive selection against the current option list.
|
|
52
52
|
*/
|
|
53
|
-
function resolveConfiguredValue(currentValue, values, configuredDefault) {
|
|
54
|
-
if (values.includes(currentValue)) return currentValue;
|
|
53
|
+
function resolveConfiguredValue(currentValue, values, configuredDefault, globalDefault) {
|
|
54
|
+
if (currentValue !== null && values.includes(currentValue)) return currentValue;
|
|
55
55
|
if (configuredDefault && values.includes(configuredDefault)) return configuredDefault;
|
|
56
|
+
if (globalDefault && values.includes(globalDefault)) return globalDefault;
|
|
56
57
|
return values[0] ?? currentValue;
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
@@ -70,8 +71,11 @@ const TIME_BUCKET_ORDER = [
|
|
|
70
71
|
*/
|
|
71
72
|
const CHART_TYPE_ORDER = [
|
|
72
73
|
"bar",
|
|
74
|
+
"grouped-bar",
|
|
75
|
+
"percent-bar",
|
|
73
76
|
"line",
|
|
74
77
|
"area",
|
|
78
|
+
"percent-area",
|
|
75
79
|
"pie",
|
|
76
80
|
"donut"
|
|
77
81
|
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/core/date-range-presets.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* All recognised date range preset identifiers.
|
|
4
|
+
*
|
|
5
|
+
* - `'auto'` — derived from the active time bucket
|
|
6
|
+
* - `'all-time'` — no date filtering
|
|
7
|
+
* - Relative presets — rolling window from "now"
|
|
8
|
+
* - Calendar presets — aligned to calendar boundaries
|
|
9
|
+
*/
|
|
10
|
+
type DateRangePresetId = 'auto' | 'all-time' | 'last-7-days' | 'last-30-days' | 'last-3-months' | 'last-12-months' | 'quarter-to-date' | 'year-to-date' | 'last-year';
|
|
11
|
+
//#endregion
|
|
12
|
+
export { DateRangePresetId };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//#region src/core/date-range-presets.ts
|
|
2
|
+
/**
|
|
3
|
+
* Ordered list of all date range presets shown in the UI.
|
|
4
|
+
*
|
|
5
|
+
* Layout hint (2-column grid):
|
|
6
|
+
* Auto | All time
|
|
7
|
+
* Last 7 days | Last 30 days
|
|
8
|
+
* Last 3 months | Last 12 months
|
|
9
|
+
* Quarter to date | Year to date
|
|
10
|
+
* Last year |
|
|
11
|
+
*/
|
|
12
|
+
const DATE_RANGE_PRESETS = [
|
|
13
|
+
{
|
|
14
|
+
id: "auto",
|
|
15
|
+
label: "Auto",
|
|
16
|
+
description: "Adjusts the date range based on the time bucket: day → last 30 days, week → last 3 months, month → last 12 months, quarter/year → all time",
|
|
17
|
+
buildFilter: () => null
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "all-time",
|
|
21
|
+
label: "All time",
|
|
22
|
+
buildFilter: () => null
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "last-7-days",
|
|
26
|
+
label: "Last 7 days",
|
|
27
|
+
buildFilter: () => ({
|
|
28
|
+
from: daysAgo(7),
|
|
29
|
+
to: null
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "last-30-days",
|
|
34
|
+
label: "Last 30 days",
|
|
35
|
+
buildFilter: () => ({
|
|
36
|
+
from: daysAgo(30),
|
|
37
|
+
to: null
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "last-3-months",
|
|
42
|
+
label: "Last 3 months",
|
|
43
|
+
buildFilter: () => ({
|
|
44
|
+
from: monthsAgo(3),
|
|
45
|
+
to: null
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "last-12-months",
|
|
50
|
+
label: "Last 12 months",
|
|
51
|
+
buildFilter: () => ({
|
|
52
|
+
from: monthsAgo(12),
|
|
53
|
+
to: null
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "quarter-to-date",
|
|
58
|
+
label: "Quarter to date",
|
|
59
|
+
buildFilter: () => ({
|
|
60
|
+
from: startOfQuarter(),
|
|
61
|
+
to: null
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "year-to-date",
|
|
66
|
+
label: "Year to date",
|
|
67
|
+
buildFilter: () => ({
|
|
68
|
+
from: startOfYear(),
|
|
69
|
+
to: null
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "last-year",
|
|
74
|
+
label: "Last year",
|
|
75
|
+
buildFilter: () => lastYear()
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Map a time bucket to a sensible default date range.
|
|
80
|
+
*
|
|
81
|
+
* - `day` → last 30 days (enough for a meaningful daily trend)
|
|
82
|
+
* - `week` → last 3 months (~13 weeks)
|
|
83
|
+
* - `month` → last 12 months
|
|
84
|
+
* - `quarter` → all time (null)
|
|
85
|
+
* - `year` → all time (null)
|
|
86
|
+
*/
|
|
87
|
+
function autoFilterForBucket(bucket) {
|
|
88
|
+
switch (bucket) {
|
|
89
|
+
case "day": return {
|
|
90
|
+
from: daysAgo(30),
|
|
91
|
+
to: null
|
|
92
|
+
};
|
|
93
|
+
case "week": return {
|
|
94
|
+
from: monthsAgo(3),
|
|
95
|
+
to: null
|
|
96
|
+
};
|
|
97
|
+
case "month": return {
|
|
98
|
+
from: monthsAgo(12),
|
|
99
|
+
to: null
|
|
100
|
+
};
|
|
101
|
+
case "quarter":
|
|
102
|
+
case "year": return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compute the effective `DateRangeFilter` for a given preset.
|
|
107
|
+
*
|
|
108
|
+
* For `'auto'`, this uses the provided `timeBucket` to derive the range.
|
|
109
|
+
* For all other presets, the filter is computed from the preset definition.
|
|
110
|
+
*
|
|
111
|
+
* @returns The resolved filter, or `null` for "all time".
|
|
112
|
+
*/
|
|
113
|
+
function resolvePresetFilter(presetId, timeBucket) {
|
|
114
|
+
if (presetId === "auto") return autoFilterForBucket(timeBucket);
|
|
115
|
+
return DATE_RANGE_PRESETS.find((p) => p.id === presetId)?.buildFilter() ?? null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the human-readable label for a preset ID.
|
|
119
|
+
*/
|
|
120
|
+
function getPresetLabel(presetId) {
|
|
121
|
+
return DATE_RANGE_PRESETS.find((p) => p.id === presetId)?.label ?? "Custom";
|
|
122
|
+
}
|
|
123
|
+
function daysAgo(n) {
|
|
124
|
+
const d = /* @__PURE__ */ new Date();
|
|
125
|
+
d.setDate(d.getDate() - n);
|
|
126
|
+
d.setHours(0, 0, 0, 0);
|
|
127
|
+
return d;
|
|
128
|
+
}
|
|
129
|
+
function monthsAgo(n) {
|
|
130
|
+
const d = /* @__PURE__ */ new Date();
|
|
131
|
+
d.setMonth(d.getMonth() - n);
|
|
132
|
+
d.setHours(0, 0, 0, 0);
|
|
133
|
+
return d;
|
|
134
|
+
}
|
|
135
|
+
function startOfQuarter() {
|
|
136
|
+
const d = /* @__PURE__ */ new Date();
|
|
137
|
+
const quarterMonth = Math.floor(d.getMonth() / 3) * 3;
|
|
138
|
+
return new Date(d.getFullYear(), quarterMonth, 1);
|
|
139
|
+
}
|
|
140
|
+
function startOfYear() {
|
|
141
|
+
const d = /* @__PURE__ */ new Date();
|
|
142
|
+
return new Date(d.getFullYear(), 0, 1);
|
|
143
|
+
}
|
|
144
|
+
function lastYear() {
|
|
145
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear() - 1;
|
|
146
|
+
return {
|
|
147
|
+
from: new Date(year, 0, 1),
|
|
148
|
+
to: new Date(year, 11, 31)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { DATE_RANGE_PRESETS, getPresetLabel, resolvePresetFilter };
|
|
@@ -1,106 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChartSchemaBuilder } from "./schema-builder.types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/core/define-chart-schema.d.ts
|
|
4
|
-
type SchemaFromSections<T, TColumns extends Record<string, unknown> | undefined, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket> = {
|
|
5
|
-
columns?: Extract<TColumns, Record<string, unknown> | undefined>;
|
|
6
|
-
xAxis?: Extract<TXAxis, XAxisConfig<ResolvedXAxisColumnIdFromSchema<T, {
|
|
7
|
-
columns?: TColumns;
|
|
8
|
-
}>> | undefined>;
|
|
9
|
-
groupBy?: Extract<TGroupBy, GroupByConfig<ResolvedGroupByColumnIdFromSchema<T, {
|
|
10
|
-
columns?: TColumns;
|
|
11
|
-
}>> | undefined>;
|
|
12
|
-
filters?: Extract<TFilters, FiltersConfig<ResolvedFilterColumnIdFromSchema<T, {
|
|
13
|
-
columns?: TColumns;
|
|
14
|
-
}>> | undefined>;
|
|
15
|
-
metric?: Extract<TMetric, MetricConfig<ResolvedMetricColumnIdFromSchema<T, {
|
|
16
|
-
columns?: TColumns;
|
|
17
|
-
}>> | undefined>;
|
|
18
|
-
chartType?: Extract<TChartType, ChartTypeConfig | undefined>;
|
|
19
|
-
timeBucket?: Extract<TTimeBucket, TimeBucketConfig | undefined>;
|
|
20
|
-
};
|
|
21
|
-
type DefineChartSchemaInput<T, TColumns extends Record<string, unknown> | undefined, TXAxis, TGroupBy, TFilters, TMetric, TChartType, TTimeBucket> = {
|
|
22
|
-
/**
|
|
23
|
-
* Shape the available chart columns.
|
|
24
|
-
*
|
|
25
|
-
* This is usually the most important part of the schema. Use it to:
|
|
26
|
-
* - rename inferred raw fields with `label`
|
|
27
|
-
* - force a field to a specific `type`
|
|
28
|
-
* - apply `format` or `formatter`
|
|
29
|
-
* - remove a raw field with `false`
|
|
30
|
-
* - add brand new derived columns with `kind: 'derived'`
|
|
31
|
-
*/
|
|
32
|
-
columns?: TColumns & ExactShape<SchemaColumnsValidationShape<T, NoInfer<TColumns>>, NoInfer<TColumns>>;
|
|
33
|
-
/**
|
|
34
|
-
* Restrict which resolved columns may be selected on the X-axis.
|
|
35
|
-
*
|
|
36
|
-
* Use this when you want to expose only a subset of possible X-axis fields.
|
|
37
|
-
*/
|
|
38
|
-
xAxis?: TXAxis & ExactShape<XAxisConfig<ResolvedXAxisColumnIdFromSchema<T, {
|
|
39
|
-
columns?: TColumns;
|
|
40
|
-
}>>, NoInfer<TXAxis>>;
|
|
41
|
-
/**
|
|
42
|
-
* Restrict which resolved columns may be used to split the chart into series.
|
|
43
|
-
*
|
|
44
|
-
* This powers grouped / multi-series charts.
|
|
45
|
-
*/
|
|
46
|
-
groupBy?: TGroupBy & ExactShape<GroupByConfig<ResolvedGroupByColumnIdFromSchema<T, {
|
|
47
|
-
columns?: TColumns;
|
|
48
|
-
}>>, NoInfer<TGroupBy>>;
|
|
49
|
-
/**
|
|
50
|
-
* Restrict which resolved columns appear in the filters UI.
|
|
51
|
-
*
|
|
52
|
-
* Only category and boolean-like columns are eligible here.
|
|
53
|
-
*/
|
|
54
|
-
filters?: TFilters & ExactShape<FiltersConfig<ResolvedFilterColumnIdFromSchema<T, {
|
|
55
|
-
columns?: TColumns;
|
|
56
|
-
}>>, NoInfer<TFilters>>;
|
|
57
|
-
/**
|
|
58
|
-
* Restrict which metrics and aggregate combinations remain selectable.
|
|
59
|
-
*
|
|
60
|
-
* Use this when you want to curate the metric dropdown rather than exposing
|
|
61
|
-
* every available numeric aggregate.
|
|
62
|
-
*/
|
|
63
|
-
metric?: TMetric & ExactShape<MetricConfig<ResolvedMetricColumnIdFromSchema<T, {
|
|
64
|
-
columns?: TColumns;
|
|
65
|
-
}>>, NoInfer<TMetric>>; /** Restrict which chart renderers are available to the user. */
|
|
66
|
-
chartType?: TChartType & ExactShape<ChartTypeConfig, NoInfer<TChartType>>;
|
|
67
|
-
/**
|
|
68
|
-
* Restrict which time buckets remain available for date X-axes.
|
|
69
|
-
*
|
|
70
|
-
* Example: allow only `'month'` and `'quarter'`.
|
|
71
|
-
*/
|
|
72
|
-
timeBucket?: TTimeBucket & ExactShape<TimeBucketConfig, NoInfer<TTimeBucket>>;
|
|
73
|
-
};
|
|
74
4
|
/**
|
|
75
|
-
* Define one explicit chart schema
|
|
5
|
+
* Define one explicit chart schema through a fluent builder API.
|
|
76
6
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* columns, while the top-level control sections restrict the public chart
|
|
80
|
-
* contract.
|
|
7
|
+
* Put `.columns(...)` early in the chain so later sections can narrow against
|
|
8
|
+
* the declared column ids and roles.
|
|
81
9
|
*
|
|
82
10
|
* Typical shape:
|
|
83
11
|
*
|
|
84
12
|
* ```ts
|
|
85
|
-
* const schema = defineChartSchema<Row>()
|
|
86
|
-
* columns
|
|
87
|
-
* createdAt
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* label: '
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* metric
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
13
|
+
* const schema = defineChartSchema<Row>()
|
|
14
|
+
* .columns((c) => [
|
|
15
|
+
* c.date('createdAt', {label: 'Created'}),
|
|
16
|
+
* c.category('ownerName', {label: 'Owner'}),
|
|
17
|
+
* c.number('salary', {format: 'currency'}),
|
|
18
|
+
* c.exclude('internalId'),
|
|
19
|
+
* c.derived.category('salaryBand', {
|
|
20
|
+
* label: 'Salary Band',
|
|
21
|
+
* accessor: row => row.salary != null && row.salary > 100_000 ? 'High' : 'Base',
|
|
22
|
+
* }),
|
|
23
|
+
* ])
|
|
24
|
+
* .xAxis((x) => x.allowed('createdAt').default('createdAt'))
|
|
25
|
+
* .groupBy((g) => g.allowed('ownerName', 'salaryBand'))
|
|
26
|
+
* .metric((m) =>
|
|
27
|
+
* m
|
|
28
|
+
* .count()
|
|
29
|
+
* .aggregate('salary', 'sum', 'avg')
|
|
30
|
+
* .defaultAggregate('salary', 'sum')
|
|
31
|
+
* )
|
|
32
|
+
*
|
|
33
|
+
* // Pass the builder directly to useChart(...) or inferColumnsFromData(...).
|
|
102
34
|
* ```
|
|
103
35
|
*/
|
|
104
|
-
declare function defineChartSchema<
|
|
36
|
+
declare function defineChartSchema<TRow>(): ChartSchemaBuilder<TRow, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined>;
|
|
105
37
|
//#endregion
|
|
106
38
|
export { defineChartSchema };
|
|
@@ -1,47 +1,39 @@
|
|
|
1
|
+
import { createChartSchemaBuilder } from "./schema-builder.mjs";
|
|
1
2
|
//#region src/core/define-chart-schema.ts
|
|
2
3
|
/**
|
|
3
|
-
* Define one explicit chart schema
|
|
4
|
+
* Define one explicit chart schema through a fluent builder API.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* columns, while the top-level control sections restrict the public chart
|
|
8
|
-
* contract.
|
|
6
|
+
* Put `.columns(...)` early in the chain so later sections can narrow against
|
|
7
|
+
* the declared column ids and roles.
|
|
9
8
|
*
|
|
10
9
|
* Typical shape:
|
|
11
10
|
*
|
|
12
11
|
* ```ts
|
|
13
|
-
* const schema = defineChartSchema<Row>()
|
|
14
|
-
* columns
|
|
15
|
-
* createdAt
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* label: '
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* metric
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
12
|
+
* const schema = defineChartSchema<Row>()
|
|
13
|
+
* .columns((c) => [
|
|
14
|
+
* c.date('createdAt', {label: 'Created'}),
|
|
15
|
+
* c.category('ownerName', {label: 'Owner'}),
|
|
16
|
+
* c.number('salary', {format: 'currency'}),
|
|
17
|
+
* c.exclude('internalId'),
|
|
18
|
+
* c.derived.category('salaryBand', {
|
|
19
|
+
* label: 'Salary Band',
|
|
20
|
+
* accessor: row => row.salary != null && row.salary > 100_000 ? 'High' : 'Base',
|
|
21
|
+
* }),
|
|
22
|
+
* ])
|
|
23
|
+
* .xAxis((x) => x.allowed('createdAt').default('createdAt'))
|
|
24
|
+
* .groupBy((g) => g.allowed('ownerName', 'salaryBand'))
|
|
25
|
+
* .metric((m) =>
|
|
26
|
+
* m
|
|
27
|
+
* .count()
|
|
28
|
+
* .aggregate('salary', 'sum', 'avg')
|
|
29
|
+
* .defaultAggregate('salary', 'sum')
|
|
30
|
+
* )
|
|
31
|
+
*
|
|
32
|
+
* // Pass the builder directly to useChart(...) or inferColumnsFromData(...).
|
|
30
33
|
* ```
|
|
31
34
|
*/
|
|
32
35
|
function defineChartSchema() {
|
|
33
|
-
|
|
34
|
-
* Brand one schema object while preserving its literal types.
|
|
35
|
-
*
|
|
36
|
-
* This is what lets the schema stay both strongly typed and editor-friendly
|
|
37
|
-
* when it is later passed to `useChart(...)`.
|
|
38
|
-
*/
|
|
39
|
-
return function defineSchema(schema) {
|
|
40
|
-
return {
|
|
41
|
-
...schema,
|
|
42
|
-
__chartSchemaBrand: "chart-schema-definition"
|
|
43
|
-
};
|
|
44
|
-
};
|
|
36
|
+
return createChartSchemaBuilder();
|
|
45
37
|
}
|
|
46
38
|
//#endregion
|
|
47
39
|
export { defineChartSchema };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ChartColumn,
|
|
1
|
+
import { ChartColumn, ChartSchemaDefinition, ResolvedChartSchemaFromDefinition, ResolvedColumnIdFromSchema } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/core/infer-columns.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Resolve chart columns directly from raw data and an optional explicit schema.
|
|
6
6
|
*/
|
|
7
|
-
declare function inferColumnsFromData<T, const TSchema extends
|
|
7
|
+
declare function inferColumnsFromData<T, const TSchema extends ChartSchemaDefinition<T, any> | undefined = undefined>(data: readonly T[], schema?: TSchema): readonly ChartColumn<T, ResolvedColumnIdFromSchema<T, ResolvedChartSchemaFromDefinition<TSchema>>>[];
|
|
8
8
|
//#endregion
|
|
9
9
|
export { inferColumnsFromData };
|