@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 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,
@@ -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
- `hsl(var(--chart-1, var(--cs-chart-1, 245 72% 57%)))`,
24
- `hsl(var(--chart-2, var(--cs-chart-2, 271 72% 55%)))`,
25
- `hsl(var(--chart-3, var(--cs-chart-3, 330 68% 54%)))`,
26
- `hsl(var(--chart-4, var(--cs-chart-4, 170 65% 38%)))`,
27
- `hsl(var(--chart-5, var(--cs-chart-5, 30 90% 54%)))`
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.
@@ -70,8 +70,11 @@ const TIME_BUCKET_ORDER = [
70
70
  */
71
71
  const CHART_TYPE_ORDER = [
72
72
  "bar",
73
+ "grouped-bar",
74
+ "percent-bar",
73
75
  "line",
74
76
  "area",
77
+ "percent-area",
75
78
  "pie",
76
79
  "donut"
77
80
  ];
@@ -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) point[group] = aggregate(groupMap.get(group) ?? [], metric.kind === "aggregate" ? metric.aggregate : "count", metric.kind === "aggregate" ? metric.includeZeros ?? true : true);
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
@@ -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; /** Active filter values per column. */
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 filter (null = all time). */
895
- dateRangeFilter: DateRangeFilter | null; /** Set the date range filter. Pass null to clear (show all time). */
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. */
@@ -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 [dateRangeFilter, setDateRangeFilter] = useState(null);
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
  /**