@matthieumordrel/chart-studio 0.2.2 → 0.2.4
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 +24 -0
- 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 +3 -0
- package/dist/core/date-range-presets.d.mts +12 -0
- package/dist/core/date-range-presets.mjs +152 -0
- package/dist/core/pipeline-data-points.mjs +4 -1
- package/dist/core/types.d.mts +37 -6
- package/dist/core/use-chart.mjs +50 -25
- package/dist/ui/chart-canvas.d.mts +1 -1
- package/dist/ui/chart-canvas.mjs +305 -27
- package/dist/ui/chart-context.d.mts +3 -0
- 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-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-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/theme.css +54 -49
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -364,6 +364,30 @@ If you are importing the package source directly in a local playground or monore
|
|
|
364
364
|
|
|
365
365
|
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.
|
|
366
366
|
|
|
367
|
+
## On the Radar
|
|
368
|
+
|
|
369
|
+
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.
|
|
370
|
+
|
|
371
|
+
### Renderer flexibility
|
|
372
|
+
|
|
373
|
+
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.
|
|
374
|
+
|
|
375
|
+
### Richer aggregation
|
|
376
|
+
|
|
377
|
+
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).
|
|
378
|
+
|
|
379
|
+
### Chart interactivity
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
### Multi-dataset composition
|
|
384
|
+
|
|
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
|
+
|
|
387
|
+
### The double-call schema syntax
|
|
388
|
+
|
|
389
|
+
`defineChartSchema<Row>()()` uses a double function call as a workaround for TypeScript's lack of partial type argument inference. This lets you provide the row type explicitly while the column IDs are inferred automatically. It works well but can surprise newcomers — this will be revisited if TypeScript adds native support for partial inference.
|
|
390
|
+
|
|
367
391
|
## Release
|
|
368
392
|
|
|
369
393
|
- `bun run release:check`
|
|
@@ -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
|
-
`
|
|
24
|
-
`
|
|
25
|
-
`
|
|
26
|
-
`
|
|
27
|
-
`
|
|
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.
|
|
@@ -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 };
|
|
@@ -157,7 +157,10 @@ function buildTimeBuckets(items, xColumn, groupByColumn, groups, metric, metricC
|
|
|
157
157
|
xKey: key
|
|
158
158
|
};
|
|
159
159
|
const groupMap = accumulator.get(key);
|
|
160
|
-
for (const group of groups)
|
|
160
|
+
for (const group of groups) {
|
|
161
|
+
const values = groupMap.get(group) ?? [];
|
|
162
|
+
point[group] = values.length === 0 ? null : aggregate(values, metric.kind === "aggregate" ? metric.aggregate : "count", metric.kind === "aggregate" ? metric.includeZeros ?? true : true);
|
|
163
|
+
}
|
|
161
164
|
return point;
|
|
162
165
|
}),
|
|
163
166
|
groups
|
package/dist/core/types.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DateRangePresetId } from "./date-range-presets.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/core/types.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Core types for chart-studio.
|
|
@@ -570,9 +572,9 @@ type NumberColumn<T, TId extends string = string> = ColumnBase<T, TId> & {
|
|
|
570
572
|
/** Union of all column types. */
|
|
571
573
|
type ChartColumn<T, TId extends string = string> = DateColumn<T, TId> | CategoryColumn<T, TId> | BooleanColumn<T, TId> | NumberColumn<T, TId>;
|
|
572
574
|
/** Chart types available for time-series (date X-axis). */
|
|
573
|
-
type TimeSeriesChartType = 'bar' | 'line' | 'area';
|
|
575
|
+
type TimeSeriesChartType = 'bar' | 'grouped-bar' | 'percent-bar' | 'line' | 'area' | 'percent-area';
|
|
574
576
|
/** Chart types available for categorical (category/boolean X-axis). */
|
|
575
|
-
type CategoricalChartType = 'bar' | 'pie' | 'donut';
|
|
577
|
+
type CategoricalChartType = 'bar' | 'grouped-bar' | 'percent-bar' | 'pie' | 'donut';
|
|
576
578
|
/** All supported chart types. */
|
|
577
579
|
type ChartType = TimeSeriesChartType | CategoricalChartType;
|
|
578
580
|
/** Time bucket sizes for date X-axis. */
|
|
@@ -706,6 +708,18 @@ type ChartSchema<T, TColumns extends Record<string, unknown> | undefined = Recor
|
|
|
706
708
|
metric?: MetricConfig<string>; /** Restrict which chart types are available to the user. */
|
|
707
709
|
chartType?: ChartTypeConfig; /** Restrict which time buckets are available for date X-axes. */
|
|
708
710
|
timeBucket?: TimeBucketConfig;
|
|
711
|
+
/**
|
|
712
|
+
* Whether line and area charts should connect across null (empty bucket)
|
|
713
|
+
* data points instead of showing a gap.
|
|
714
|
+
*
|
|
715
|
+
* When `true` (default), the line bridges across empty buckets.
|
|
716
|
+
* When `false`, empty time buckets produce a visible gap in the line/area.
|
|
717
|
+
*
|
|
718
|
+
* This is useful for sparse datasets (e.g. quarterly data displayed in a
|
|
719
|
+
* monthly time bucket) where connecting across gaps produces a cleaner
|
|
720
|
+
* visual than showing drops to zero.
|
|
721
|
+
*/
|
|
722
|
+
connectNulls?: boolean;
|
|
709
723
|
};
|
|
710
724
|
type ChartSchemaDefinitionBrand = {
|
|
711
725
|
readonly __chartSchemaBrand: 'chart-schema-definition';
|
|
@@ -767,7 +781,7 @@ type ChartSeries = {
|
|
|
767
781
|
* A single data point in the transformed output.
|
|
768
782
|
* Keys are dynamic based on groupBy values.
|
|
769
783
|
*/
|
|
770
|
-
type TransformedDataPoint = Record<string, string | number>;
|
|
784
|
+
type TransformedDataPoint = Record<string, string | number | null>;
|
|
771
785
|
/**
|
|
772
786
|
* Available filter options extracted from the data for a column.
|
|
773
787
|
*
|
|
@@ -869,7 +883,12 @@ type ChartInstance<T, TColumnId extends string = string, TChartType extends Char
|
|
|
869
883
|
timeBucket: TTimeBucket; /** Change the time bucket. Runtime accepts only values in `availableTimeBuckets`. */
|
|
870
884
|
setTimeBucket: (bucket: TTimeBucket) => void; /** Time buckets currently available for the active chart state and config. */
|
|
871
885
|
availableTimeBuckets: TTimeBucket[]; /** Whether time bucketing controls should be shown. */
|
|
872
|
-
isTimeSeries: boolean;
|
|
886
|
+
isTimeSeries: boolean;
|
|
887
|
+
/**
|
|
888
|
+
* Whether line and area charts connect across null data points.
|
|
889
|
+
* Derived from the schema's `connectNulls` option.
|
|
890
|
+
*/
|
|
891
|
+
connectNulls: boolean; /** Active filter values per column. */
|
|
873
892
|
filters: FilterState<TFilterColumnId>;
|
|
874
893
|
/**
|
|
875
894
|
* Toggle a specific filter value on/off for a column.
|
|
@@ -891,8 +910,20 @@ type ChartInstance<T, TColumnId extends string = string, TChartType extends Char
|
|
|
891
910
|
availableDateColumns: Array<{
|
|
892
911
|
id: TDateColumnId;
|
|
893
912
|
label: string;
|
|
894
|
-
}>; /** Active date range
|
|
895
|
-
|
|
913
|
+
}>; /** Active date range preset (null = custom range via `dateRangeFilter`). */
|
|
914
|
+
dateRangePreset: DateRangePresetId | null;
|
|
915
|
+
/**
|
|
916
|
+
* Select a named date range preset.
|
|
917
|
+
* The `dateRangeFilter` is derived automatically from the preset.
|
|
918
|
+
* For `'auto'`, the filter adjusts reactively when the time bucket changes.
|
|
919
|
+
*/
|
|
920
|
+
setDateRangePreset: (preset: DateRangePresetId) => void; /** Active date range filter (null = all time). Derived from the preset when one is active. */
|
|
921
|
+
dateRangeFilter: DateRangeFilter | null;
|
|
922
|
+
/**
|
|
923
|
+
* Set the date range filter directly (custom range).
|
|
924
|
+
* Clears any active preset — the range becomes "Custom".
|
|
925
|
+
* Pass null to clear (show all time, equivalent to selecting 'all-time' preset).
|
|
926
|
+
*/
|
|
896
927
|
setDateRangeFilter: (filter: DateRangeFilter | null) => void; /** Transformed data points ready for recharts. */
|
|
897
928
|
transformedData: TransformedDataPoint[]; /** Auto-generated series definitions for recharts. */
|
|
898
929
|
series: ChartSeries[]; /** Active columns for the current source. */
|
package/dist/core/use-chart.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TIME_BUCKET_ORDER, resolveConfiguredIdSelection, resolveConfiguredValue, restrictConfiguredIdOptions, restrictConfiguredValues } from "./config-utils.mjs";
|
|
2
2
|
import { CHART_TYPE_CONFIG, getAvailableChartTypes } from "./chart-capabilities.mjs";
|
|
3
|
+
import { resolvePresetFilter } from "./date-range-presets.mjs";
|
|
3
4
|
import { inferColumnsFromData } from "./infer-columns.mjs";
|
|
4
5
|
import { DEFAULT_METRIC, buildAvailableMetrics, isSameMetric, resolveMetric, restrictAvailableMetrics } from "./metric-utils.mjs";
|
|
5
6
|
import { applyFilters, extractAvailableFilters, runPipeline } from "./pipeline.mjs";
|
|
@@ -45,7 +46,8 @@ function useChart(options) {
|
|
|
45
46
|
const [filters, setFilters] = useState(() => /* @__PURE__ */ new Map());
|
|
46
47
|
const [sorting, setSorting] = useState(null);
|
|
47
48
|
const [referenceDateIdRaw, setReferenceDateIdRaw] = useState(null);
|
|
48
|
-
const [
|
|
49
|
+
const [dateRangePreset, setDateRangePresetRaw] = useState("all-time");
|
|
50
|
+
const [customDateRangeFilter, setCustomDateRangeFilter] = useState(null);
|
|
49
51
|
const sourceIds = useMemo(() => new Set(sources.map((source) => source.id)), [sources]);
|
|
50
52
|
const activeSourceId = sourceIds.has(activeSourceIdRaw) ? activeSourceIdRaw : sources[0]?.id ?? "default";
|
|
51
53
|
const setActiveSource = (sourceId) => {
|
|
@@ -82,17 +84,6 @@ function useChart(options) {
|
|
|
82
84
|
resolvedXAxisId,
|
|
83
85
|
isTimeSeries
|
|
84
86
|
]);
|
|
85
|
-
const effectiveData = useMemo(() => {
|
|
86
|
-
const column = dateColumns.find((candidate) => candidate.id === referenceDateId);
|
|
87
|
-
if (!column) return rawData;
|
|
88
|
-
if (dateRangeFilter === null) return rawData;
|
|
89
|
-
return filterByDateRange(rawData, column, dateRangeFilter);
|
|
90
|
-
}, [
|
|
91
|
-
rawData,
|
|
92
|
-
dateRangeFilter,
|
|
93
|
-
dateColumns,
|
|
94
|
-
referenceDateId
|
|
95
|
-
]);
|
|
96
87
|
const availableGroupBys = useMemo(() => restrictConfiguredIdOptions(activeColumns.filter((column) => (column.type === "category" || column.type === "boolean") && column.id !== resolvedXAxisId).map((column) => ({
|
|
97
88
|
id: column.id,
|
|
98
89
|
label: column.label
|
|
@@ -115,19 +106,6 @@ function useChart(options) {
|
|
|
115
106
|
availableMetrics,
|
|
116
107
|
activeSource.schema
|
|
117
108
|
]);
|
|
118
|
-
const availableFilters = useMemo(() => {
|
|
119
|
-
return restrictConfiguredIdOptions(extractAvailableFilters(effectiveData, activeColumns).map((filter) => ({
|
|
120
|
-
...filter,
|
|
121
|
-
id: filter.columnId
|
|
122
|
-
})), activeSource.schema?.filters).map(({ id: _id, ...filter }) => filter);
|
|
123
|
-
}, [
|
|
124
|
-
effectiveData,
|
|
125
|
-
activeColumns,
|
|
126
|
-
activeSource.schema
|
|
127
|
-
]);
|
|
128
|
-
const availableFilterValues = useMemo(() => createAvailableFilterValueMap(availableFilters), [availableFilters]);
|
|
129
|
-
const filterColumns = useMemo(() => activeColumns.filter((column) => availableFilters.some((filter) => filter.columnId === column.id)), [activeColumns, availableFilters]);
|
|
130
|
-
const resolvedFilters = useMemo(() => sanitizeFilters(filters, filterColumns), [filters, filterColumns]);
|
|
131
109
|
const availableChartTypes = useMemo(() => restrictConfiguredValues(getAvailableChartTypes({
|
|
132
110
|
xAxisType: resolvedXAxisType,
|
|
133
111
|
hasGroupBy: resolvedGroupById !== null
|
|
@@ -154,6 +132,38 @@ function useChart(options) {
|
|
|
154
132
|
availableTimeBuckets,
|
|
155
133
|
activeSource.schema
|
|
156
134
|
]);
|
|
135
|
+
const dateRangeFilter = useMemo(() => {
|
|
136
|
+
if (dateRangePreset !== null) return resolvePresetFilter(dateRangePreset, resolvedTimeBucket);
|
|
137
|
+
return customDateRangeFilter;
|
|
138
|
+
}, [
|
|
139
|
+
dateRangePreset,
|
|
140
|
+
customDateRangeFilter,
|
|
141
|
+
resolvedTimeBucket
|
|
142
|
+
]);
|
|
143
|
+
const effectiveData = useMemo(() => {
|
|
144
|
+
const column = dateColumns.find((candidate) => candidate.id === referenceDateId);
|
|
145
|
+
if (!column) return rawData;
|
|
146
|
+
if (dateRangeFilter === null) return rawData;
|
|
147
|
+
return filterByDateRange(rawData, column, dateRangeFilter);
|
|
148
|
+
}, [
|
|
149
|
+
rawData,
|
|
150
|
+
dateRangeFilter,
|
|
151
|
+
dateColumns,
|
|
152
|
+
referenceDateId
|
|
153
|
+
]);
|
|
154
|
+
const availableFilters = useMemo(() => {
|
|
155
|
+
return restrictConfiguredIdOptions(extractAvailableFilters(effectiveData, activeColumns).map((filter) => ({
|
|
156
|
+
...filter,
|
|
157
|
+
id: filter.columnId
|
|
158
|
+
})), activeSource.schema?.filters).map(({ id: _id, ...filter }) => filter);
|
|
159
|
+
}, [
|
|
160
|
+
effectiveData,
|
|
161
|
+
activeColumns,
|
|
162
|
+
activeSource.schema
|
|
163
|
+
]);
|
|
164
|
+
const availableFilterValues = useMemo(() => createAvailableFilterValueMap(availableFilters), [availableFilters]);
|
|
165
|
+
const filterColumns = useMemo(() => activeColumns.filter((column) => availableFilters.some((filter) => filter.columnId === column.id)), [activeColumns, availableFilters]);
|
|
166
|
+
const resolvedFilters = useMemo(() => sanitizeFilters(filters, filterColumns), [filters, filterColumns]);
|
|
157
167
|
const pipelineResult = useMemo(() => {
|
|
158
168
|
if (!resolvedXAxisId) return {
|
|
159
169
|
data: [],
|
|
@@ -249,6 +259,18 @@ function useChart(options) {
|
|
|
249
259
|
if (!availableDateColumnIds.has(columnId)) return;
|
|
250
260
|
setReferenceDateIdRaw(columnId);
|
|
251
261
|
};
|
|
262
|
+
const setDateRangePreset = (preset) => {
|
|
263
|
+
setDateRangePresetRaw(preset);
|
|
264
|
+
};
|
|
265
|
+
const setDateRangeFilter = (filter) => {
|
|
266
|
+
if (filter === null) {
|
|
267
|
+
setDateRangePresetRaw("all-time");
|
|
268
|
+
setCustomDateRangeFilter(null);
|
|
269
|
+
} else {
|
|
270
|
+
setDateRangePresetRaw(null);
|
|
271
|
+
setCustomDateRangeFilter(filter);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
252
274
|
const chart = {
|
|
253
275
|
activeSourceId,
|
|
254
276
|
setActiveSource,
|
|
@@ -273,6 +295,7 @@ function useChart(options) {
|
|
|
273
295
|
setTimeBucket,
|
|
274
296
|
availableTimeBuckets,
|
|
275
297
|
isTimeSeries,
|
|
298
|
+
connectNulls: activeSource.schema?.connectNulls ?? true,
|
|
276
299
|
filters: resolvedFilters,
|
|
277
300
|
toggleFilter,
|
|
278
301
|
clearFilter,
|
|
@@ -284,6 +307,8 @@ function useChart(options) {
|
|
|
284
307
|
referenceDateId,
|
|
285
308
|
setReferenceDateId,
|
|
286
309
|
availableDateColumns,
|
|
310
|
+
dateRangePreset,
|
|
311
|
+
setDateRangePreset,
|
|
287
312
|
dateRangeFilter,
|
|
288
313
|
setDateRangeFilter,
|
|
289
314
|
transformedData: pipelineResult.data,
|
|
@@ -4,7 +4,7 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
4
4
|
/**
|
|
5
5
|
* Chart canvas — renders the actual recharts chart based on the current state.
|
|
6
6
|
*
|
|
7
|
-
* Supports: bar, line, area (time-series), bar, pie, donut (categorical).
|
|
7
|
+
* Supports: bar, grouped-bar, percent-bar, line, area, percent-area (time-series), bar, grouped-bar, percent-bar, pie, donut (categorical).
|
|
8
8
|
* Automatically switches between chart types based on the chart instance state.
|
|
9
9
|
*/
|
|
10
10
|
/**
|