@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/dist/ui/chart-canvas.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { Area, AreaChart, Bar, BarChart, CartesianGrid, LabelList, Legend, Line,
|
|
|
9
9
|
/**
|
|
10
10
|
* Chart canvas — renders the actual recharts chart based on the current state.
|
|
11
11
|
*
|
|
12
|
-
* Supports: bar, line, area (time-series), bar, pie, donut (categorical).
|
|
12
|
+
* Supports: bar, grouped-bar, percent-bar, line, area, percent-area (time-series), bar, grouped-bar, percent-bar, pie, donut (categorical).
|
|
13
13
|
* Automatically switches between chart types based on the chart instance state.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
@@ -179,10 +179,44 @@ function useContainerWidth() {
|
|
|
179
179
|
width
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Read the resolved `--cs-radius` CSS variable and convert it to a pixel value
|
|
184
|
+
* suitable for recharts bar corner radius (roughly half the theme radius).
|
|
185
|
+
* Observes style attribute mutations on the root element so the chart reacts
|
|
186
|
+
* when the consumer changes `--radius` at runtime.
|
|
187
|
+
*/
|
|
188
|
+
function useCssBarRadius() {
|
|
189
|
+
const [radiusPx, setRadiusPx] = useState(4);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (typeof document === "undefined") return;
|
|
192
|
+
const root = document.documentElement;
|
|
193
|
+
function read() {
|
|
194
|
+
const style = getComputedStyle(root);
|
|
195
|
+
const raw = style.getPropertyValue("--cs-radius").trim();
|
|
196
|
+
if (!raw) return;
|
|
197
|
+
const rem = parseFloat(raw);
|
|
198
|
+
if (Number.isNaN(rem)) return;
|
|
199
|
+
const fontSize = parseFloat(style.fontSize) || 16;
|
|
200
|
+
setRadiusPx(Math.max(0, Math.round(rem * fontSize / 2)));
|
|
201
|
+
}
|
|
202
|
+
read();
|
|
203
|
+
const observer = new MutationObserver(read);
|
|
204
|
+
observer.observe(root, {
|
|
205
|
+
attributes: true,
|
|
206
|
+
attributeFilter: [
|
|
207
|
+
"style",
|
|
208
|
+
"class",
|
|
209
|
+
"data-theme"
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
return () => observer.disconnect();
|
|
213
|
+
}, []);
|
|
214
|
+
return radiusPx;
|
|
215
|
+
}
|
|
182
216
|
/** Renders the appropriate recharts chart based on the chart instance state. */
|
|
183
217
|
function ChartCanvas({ height = 300, className, showDataLabels = false }) {
|
|
184
218
|
const chart = useChartContext();
|
|
185
|
-
const { chartType, transformedData, series } = chart;
|
|
219
|
+
const { chartType, transformedData, series, connectNulls } = chart;
|
|
186
220
|
const { ref, width } = useContainerWidth();
|
|
187
221
|
const xColumn = chart.columns.find((column) => column.id === chart.xAxisId) ?? null;
|
|
188
222
|
const aggregateMetric = chart.metric.kind === "aggregate" ? chart.metric : null;
|
|
@@ -217,7 +251,8 @@ function ChartCanvas({ height = 300, className, showDataLabels = false }) {
|
|
|
217
251
|
allowDecimalTicks,
|
|
218
252
|
xColumn,
|
|
219
253
|
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
220
|
-
showDataLabels
|
|
254
|
+
showDataLabels,
|
|
255
|
+
connectNulls
|
|
221
256
|
}) : chartType === "line" ? /* @__PURE__ */ jsx(LineChartRenderer, {
|
|
222
257
|
data: transformedData,
|
|
223
258
|
series,
|
|
@@ -228,7 +263,20 @@ function ChartCanvas({ height = 300, className, showDataLabels = false }) {
|
|
|
228
263
|
allowDecimalTicks,
|
|
229
264
|
xColumn,
|
|
230
265
|
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
231
|
-
showDataLabels
|
|
266
|
+
showDataLabels,
|
|
267
|
+
connectNulls
|
|
268
|
+
}) : chartType === "percent-area" ? /* @__PURE__ */ jsx(PercentAreaChartRenderer, {
|
|
269
|
+
data: transformedData,
|
|
270
|
+
series,
|
|
271
|
+
width,
|
|
272
|
+
height,
|
|
273
|
+
valueColumn,
|
|
274
|
+
valueRange,
|
|
275
|
+
allowDecimalTicks,
|
|
276
|
+
xColumn,
|
|
277
|
+
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
278
|
+
showDataLabels,
|
|
279
|
+
connectNulls
|
|
232
280
|
}) : chartType === "area" ? /* @__PURE__ */ jsx(AreaChartRenderer, {
|
|
233
281
|
data: transformedData,
|
|
234
282
|
series,
|
|
@@ -239,7 +287,32 @@ function ChartCanvas({ height = 300, className, showDataLabels = false }) {
|
|
|
239
287
|
allowDecimalTicks,
|
|
240
288
|
xColumn,
|
|
241
289
|
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
242
|
-
showDataLabels
|
|
290
|
+
showDataLabels,
|
|
291
|
+
connectNulls
|
|
292
|
+
}) : chartType === "grouped-bar" ? /* @__PURE__ */ jsx(GroupedBarChartRenderer, {
|
|
293
|
+
data: transformedData,
|
|
294
|
+
series,
|
|
295
|
+
width,
|
|
296
|
+
height,
|
|
297
|
+
valueColumn,
|
|
298
|
+
valueRange,
|
|
299
|
+
allowDecimalTicks,
|
|
300
|
+
xColumn,
|
|
301
|
+
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
302
|
+
showDataLabels,
|
|
303
|
+
connectNulls
|
|
304
|
+
}) : chartType === "percent-bar" ? /* @__PURE__ */ jsx(PercentBarChartRenderer, {
|
|
305
|
+
data: transformedData,
|
|
306
|
+
series,
|
|
307
|
+
width,
|
|
308
|
+
height,
|
|
309
|
+
valueColumn,
|
|
310
|
+
valueRange,
|
|
311
|
+
allowDecimalTicks,
|
|
312
|
+
xColumn,
|
|
313
|
+
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
314
|
+
showDataLabels,
|
|
315
|
+
connectNulls
|
|
243
316
|
}) : /* @__PURE__ */ jsx(BarChartRenderer, {
|
|
244
317
|
data: transformedData,
|
|
245
318
|
series,
|
|
@@ -250,11 +323,39 @@ function ChartCanvas({ height = 300, className, showDataLabels = false }) {
|
|
|
250
323
|
allowDecimalTicks,
|
|
251
324
|
xColumn,
|
|
252
325
|
timeBucket: chart.isTimeSeries ? chart.timeBucket : void 0,
|
|
253
|
-
showDataLabels
|
|
326
|
+
showDataLabels,
|
|
327
|
+
connectNulls
|
|
254
328
|
}))
|
|
255
329
|
});
|
|
256
330
|
}
|
|
257
331
|
/**
|
|
332
|
+
* Percent-stacked charts use stackOffset="expand" which normalizes values to
|
|
333
|
+
* the 0–1 range. These constants let the shared formatting pipeline treat
|
|
334
|
+
* them as proper percentages via Intl.NumberFormat({ style: 'percent' }).
|
|
335
|
+
*/
|
|
336
|
+
const PERCENT_STACKED_COLUMN = {
|
|
337
|
+
type: "number",
|
|
338
|
+
format: "percent",
|
|
339
|
+
formatter: void 0
|
|
340
|
+
};
|
|
341
|
+
const PERCENT_STACKED_RANGE = {
|
|
342
|
+
min: 0,
|
|
343
|
+
max: 1
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Remove data points where every series value is null.
|
|
347
|
+
*
|
|
348
|
+
* Stacked charts (percent-area, percent-bar) cannot represent null in the
|
|
349
|
+
* stack — d3's stack layout coerces missing values to 0 which distorts the
|
|
350
|
+
* visual. Dropping entirely-empty buckets lets `connectNulls` bridge the
|
|
351
|
+
* gap while keeping partially-populated buckets intact (null → 0 is
|
|
352
|
+
* acceptable there because the segment genuinely contributes nothing to the
|
|
353
|
+
* total).
|
|
354
|
+
*/
|
|
355
|
+
function filterAllNullPoints(data, series) {
|
|
356
|
+
return data.filter((point) => series.some((s) => point[s.dataKey] != null));
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
258
359
|
* Shared shell for all Cartesian chart types.
|
|
259
360
|
* Owns the grid, axes, tooltip, and legend — the only things that change
|
|
260
361
|
* per chart type are the root component and the series element.
|
|
@@ -336,30 +437,37 @@ function formatXAxisValue(value, xColumn, timeBucket, surface) {
|
|
|
336
437
|
}
|
|
337
438
|
function BarChartRenderer(props) {
|
|
338
439
|
const { series, showDataLabels, valueColumn, valueRange } = props;
|
|
440
|
+
const barRadius = useCssBarRadius();
|
|
441
|
+
const isStacked = series.length > 1;
|
|
442
|
+
const topSeriesKey = series[series.length - 1]?.dataKey;
|
|
339
443
|
return /* @__PURE__ */ jsx(CartesianChartShell, {
|
|
340
444
|
...props,
|
|
341
445
|
Chart: BarChart,
|
|
342
|
-
renderSeries: (s) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
446
|
+
renderSeries: (s) => {
|
|
447
|
+
const isTop = !isStacked || s.dataKey === topSeriesKey;
|
|
448
|
+
return /* @__PURE__ */ jsx(Bar, {
|
|
449
|
+
dataKey: s.dataKey,
|
|
450
|
+
name: s.label,
|
|
451
|
+
fill: s.color,
|
|
452
|
+
fillOpacity: .7,
|
|
453
|
+
radius: isTop ? [
|
|
454
|
+
barRadius,
|
|
455
|
+
barRadius,
|
|
456
|
+
0,
|
|
457
|
+
0
|
|
458
|
+
] : 0,
|
|
459
|
+
stackId: isStacked ? "stack" : void 0,
|
|
460
|
+
children: showDataLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
461
|
+
position: "top",
|
|
462
|
+
offset: 8,
|
|
463
|
+
formatter: (value) => formatDataLabel(value, valueColumn, valueRange)
|
|
464
|
+
})
|
|
465
|
+
}, s.dataKey);
|
|
466
|
+
}
|
|
359
467
|
});
|
|
360
468
|
}
|
|
361
469
|
function LineChartRenderer(props) {
|
|
362
|
-
const { showDataLabels, valueColumn, valueRange } = props;
|
|
470
|
+
const { showDataLabels, valueColumn, valueRange, connectNulls } = props;
|
|
363
471
|
return /* @__PURE__ */ jsx(CartesianChartShell, {
|
|
364
472
|
...props,
|
|
365
473
|
Chart: LineChart,
|
|
@@ -371,6 +479,7 @@ function LineChartRenderer(props) {
|
|
|
371
479
|
strokeWidth: 2,
|
|
372
480
|
dot: { r: 3 },
|
|
373
481
|
activeDot: { r: 5 },
|
|
482
|
+
connectNulls,
|
|
374
483
|
children: showDataLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
375
484
|
position: "top",
|
|
376
485
|
offset: 8,
|
|
@@ -380,7 +489,7 @@ function LineChartRenderer(props) {
|
|
|
380
489
|
});
|
|
381
490
|
}
|
|
382
491
|
function AreaChartRenderer(props) {
|
|
383
|
-
const {
|
|
492
|
+
const { showDataLabels, valueColumn, valueRange, connectNulls } = props;
|
|
384
493
|
return /* @__PURE__ */ jsx(CartesianChartShell, {
|
|
385
494
|
...props,
|
|
386
495
|
Chart: AreaChart,
|
|
@@ -391,7 +500,7 @@ function AreaChartRenderer(props) {
|
|
|
391
500
|
stroke: s.color,
|
|
392
501
|
fill: s.color,
|
|
393
502
|
fillOpacity: .3,
|
|
394
|
-
|
|
503
|
+
connectNulls,
|
|
395
504
|
children: showDataLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
396
505
|
position: "top",
|
|
397
506
|
offset: 8,
|
|
@@ -400,6 +509,175 @@ function AreaChartRenderer(props) {
|
|
|
400
509
|
}, s.dataKey)
|
|
401
510
|
});
|
|
402
511
|
}
|
|
512
|
+
function PercentAreaChartRenderer(props) {
|
|
513
|
+
const { series, data, xColumn, timeBucket, showDataLabels, connectNulls, width, height } = props;
|
|
514
|
+
const stackableData = filterAllNullPoints(data, series);
|
|
515
|
+
const yAxisWidth = estimateYAxisWidth(PERCENT_STACKED_RANGE, PERCENT_STACKED_COLUMN);
|
|
516
|
+
const xAxisTickValues = selectVisibleXAxisTicks({
|
|
517
|
+
values: stackableData.map(getXAxisTickValue),
|
|
518
|
+
labels: stackableData.map((point) => formatXAxisValue(getXAxisTickValue(point), xColumn, timeBucket, "axis")),
|
|
519
|
+
plotWidth: getCartesianPlotWidth(width, yAxisWidth),
|
|
520
|
+
minimumTickGap: X_AXIS_MINIMUM_TICK_GAP,
|
|
521
|
+
measureLabelWidth: measureAxisLabelWidth
|
|
522
|
+
});
|
|
523
|
+
return /* @__PURE__ */ jsxs(AreaChart, {
|
|
524
|
+
data: stackableData,
|
|
525
|
+
width,
|
|
526
|
+
height,
|
|
527
|
+
margin: getCartesianChartMargin(showDataLabels),
|
|
528
|
+
stackOffset: "expand",
|
|
529
|
+
children: [
|
|
530
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
531
|
+
vertical: false,
|
|
532
|
+
strokeDasharray: "3 3"
|
|
533
|
+
}),
|
|
534
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
535
|
+
dataKey: "xKey",
|
|
536
|
+
tickLine: false,
|
|
537
|
+
axisLine: false,
|
|
538
|
+
tickMargin: 8,
|
|
539
|
+
interval: 0,
|
|
540
|
+
padding: CARTESIAN_X_AXIS_PADDING,
|
|
541
|
+
ticks: xAxisTickValues,
|
|
542
|
+
tickFormatter: (value) => formatXAxisValue(value, xColumn, timeBucket, "axis")
|
|
543
|
+
}),
|
|
544
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
545
|
+
tickLine: false,
|
|
546
|
+
axisLine: false,
|
|
547
|
+
tickMargin: 4,
|
|
548
|
+
tickFormatter: (value) => typeof value === "number" ? formatChartValue(value, {
|
|
549
|
+
column: PERCENT_STACKED_COLUMN,
|
|
550
|
+
surface: "axis",
|
|
551
|
+
numericRange: PERCENT_STACKED_RANGE
|
|
552
|
+
}) : String(value),
|
|
553
|
+
width: yAxisWidth
|
|
554
|
+
}),
|
|
555
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
556
|
+
formatter: (value) => typeof value === "number" ? formatChartValue(value, {
|
|
557
|
+
column: PERCENT_STACKED_COLUMN,
|
|
558
|
+
surface: "tooltip",
|
|
559
|
+
numericRange: PERCENT_STACKED_RANGE
|
|
560
|
+
}) : value,
|
|
561
|
+
labelFormatter: (label, payload) => formatTooltipLabel(label, payload, xColumn, timeBucket)
|
|
562
|
+
}),
|
|
563
|
+
series.length > 1 && /* @__PURE__ */ jsx(Legend, {}),
|
|
564
|
+
series.map((s) => /* @__PURE__ */ jsx(Area, {
|
|
565
|
+
type: "monotone",
|
|
566
|
+
dataKey: s.dataKey,
|
|
567
|
+
name: s.label,
|
|
568
|
+
stroke: s.color,
|
|
569
|
+
fill: s.color,
|
|
570
|
+
fillOpacity: .3,
|
|
571
|
+
stackId: "percent",
|
|
572
|
+
connectNulls,
|
|
573
|
+
children: showDataLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
574
|
+
position: "top",
|
|
575
|
+
offset: 8,
|
|
576
|
+
formatter: (value) => formatDataLabel(value, PERCENT_STACKED_COLUMN, PERCENT_STACKED_RANGE)
|
|
577
|
+
})
|
|
578
|
+
}, s.dataKey))
|
|
579
|
+
]
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function GroupedBarChartRenderer(props) {
|
|
583
|
+
const { showDataLabels, valueColumn, valueRange } = props;
|
|
584
|
+
const barRadius = useCssBarRadius();
|
|
585
|
+
return /* @__PURE__ */ jsx(CartesianChartShell, {
|
|
586
|
+
...props,
|
|
587
|
+
Chart: BarChart,
|
|
588
|
+
renderSeries: (s) => /* @__PURE__ */ jsx(Bar, {
|
|
589
|
+
dataKey: s.dataKey,
|
|
590
|
+
name: s.label,
|
|
591
|
+
fill: s.color,
|
|
592
|
+
fillOpacity: .7,
|
|
593
|
+
radius: [
|
|
594
|
+
barRadius,
|
|
595
|
+
barRadius,
|
|
596
|
+
0,
|
|
597
|
+
0
|
|
598
|
+
],
|
|
599
|
+
children: showDataLabels && /* @__PURE__ */ jsx(LabelList, {
|
|
600
|
+
position: "top",
|
|
601
|
+
offset: 8,
|
|
602
|
+
formatter: (value) => formatDataLabel(value, valueColumn, valueRange)
|
|
603
|
+
})
|
|
604
|
+
}, s.dataKey)
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function PercentBarChartRenderer(props) {
|
|
608
|
+
const { series, data, xColumn, timeBucket, showDataLabels, width, height } = props;
|
|
609
|
+
const barRadius = useCssBarRadius();
|
|
610
|
+
const topSeriesKey = series[series.length - 1]?.dataKey;
|
|
611
|
+
const yAxisWidth = estimateYAxisWidth({
|
|
612
|
+
min: 0,
|
|
613
|
+
max: 100
|
|
614
|
+
}, {
|
|
615
|
+
type: "number",
|
|
616
|
+
format: void 0,
|
|
617
|
+
formatter: void 0
|
|
618
|
+
});
|
|
619
|
+
const xAxisTickValues = selectVisibleXAxisTicks({
|
|
620
|
+
values: data.map(getXAxisTickValue),
|
|
621
|
+
labels: data.map((point) => formatXAxisValue(getXAxisTickValue(point), xColumn, timeBucket, "axis")),
|
|
622
|
+
plotWidth: getCartesianPlotWidth(width, yAxisWidth),
|
|
623
|
+
minimumTickGap: X_AXIS_MINIMUM_TICK_GAP,
|
|
624
|
+
measureLabelWidth: measureAxisLabelWidth
|
|
625
|
+
});
|
|
626
|
+
return /* @__PURE__ */ jsxs(BarChart, {
|
|
627
|
+
data,
|
|
628
|
+
width,
|
|
629
|
+
height,
|
|
630
|
+
margin: getCartesianChartMargin(showDataLabels),
|
|
631
|
+
stackOffset: "expand",
|
|
632
|
+
children: [
|
|
633
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
634
|
+
vertical: false,
|
|
635
|
+
strokeDasharray: "3 3"
|
|
636
|
+
}),
|
|
637
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
638
|
+
dataKey: "xKey",
|
|
639
|
+
tickLine: false,
|
|
640
|
+
axisLine: false,
|
|
641
|
+
tickMargin: 8,
|
|
642
|
+
interval: 0,
|
|
643
|
+
padding: CARTESIAN_X_AXIS_PADDING,
|
|
644
|
+
ticks: xAxisTickValues,
|
|
645
|
+
tickFormatter: (value) => formatXAxisValue(value, xColumn, timeBucket, "axis")
|
|
646
|
+
}),
|
|
647
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
648
|
+
tickLine: false,
|
|
649
|
+
axisLine: false,
|
|
650
|
+
tickMargin: 4,
|
|
651
|
+
tickFormatter: (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : String(value),
|
|
652
|
+
width: yAxisWidth
|
|
653
|
+
}),
|
|
654
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
655
|
+
formatter: (value, name) => {
|
|
656
|
+
if (typeof value === "number") return [`${(value * 100).toFixed(1)}%`, name];
|
|
657
|
+
return [value, name];
|
|
658
|
+
},
|
|
659
|
+
labelFormatter: (label, payload) => formatTooltipLabel(label, payload, xColumn, timeBucket)
|
|
660
|
+
}),
|
|
661
|
+
series.length > 1 && /* @__PURE__ */ jsx(Legend, {}),
|
|
662
|
+
series.map((s) => {
|
|
663
|
+
const isTop = s.dataKey === topSeriesKey;
|
|
664
|
+
return /* @__PURE__ */ jsx(Bar, {
|
|
665
|
+
dataKey: s.dataKey,
|
|
666
|
+
name: s.label,
|
|
667
|
+
fill: s.color,
|
|
668
|
+
fillOpacity: .7,
|
|
669
|
+
radius: isTop ? [
|
|
670
|
+
barRadius,
|
|
671
|
+
barRadius,
|
|
672
|
+
0,
|
|
673
|
+
0
|
|
674
|
+
] : 0,
|
|
675
|
+
stackId: "percent"
|
|
676
|
+
}, s.dataKey);
|
|
677
|
+
})
|
|
678
|
+
]
|
|
679
|
+
});
|
|
680
|
+
}
|
|
403
681
|
function PieChartRenderer({ data, series, innerRadius, width, height, valueColumn, valueRange, xColumn, timeBucket, showDataLabels }) {
|
|
404
682
|
const valueKey = series[0]?.dataKey;
|
|
405
683
|
const pieData = data.map((point, index) => {
|
|
@@ -455,7 +733,7 @@ function formatDataLabel(value, valueColumn, valueRange) {
|
|
|
455
733
|
* default while tooltips and raw values remain unchanged.
|
|
456
734
|
*/
|
|
457
735
|
function shouldHideDataLabel(value) {
|
|
458
|
-
return typeof value === "number" && value === 0;
|
|
736
|
+
return value == null || typeof value === "number" && value === 0;
|
|
459
737
|
}
|
|
460
738
|
//#endregion
|
|
461
739
|
export { ChartCanvas };
|
|
@@ -35,6 +35,7 @@ type AnyChartInstance = {
|
|
|
35
35
|
setTimeBucket: (...args: any[]) => unknown;
|
|
36
36
|
availableTimeBuckets: ChartContextChart['availableTimeBuckets'];
|
|
37
37
|
isTimeSeries: boolean;
|
|
38
|
+
connectNulls: boolean;
|
|
38
39
|
filters: Map<any, Set<string>>;
|
|
39
40
|
toggleFilter: (...args: any[]) => unknown;
|
|
40
41
|
clearFilter: (...args: any[]) => unknown;
|
|
@@ -46,6 +47,8 @@ type AnyChartInstance = {
|
|
|
46
47
|
referenceDateId: string | null;
|
|
47
48
|
setReferenceDateId: (...args: any[]) => unknown;
|
|
48
49
|
availableDateColumns: ChartContextChart['availableDateColumns'];
|
|
50
|
+
dateRangePreset: ChartContextChart['dateRangePreset'];
|
|
51
|
+
setDateRangePreset: (...args: any[]) => unknown;
|
|
49
52
|
dateRangeFilter: ChartContextChart['dateRangeFilter'];
|
|
50
53
|
setDateRangeFilter: (...args: any[]) => unknown;
|
|
51
54
|
transformedData: ChartContextChart['transformedData'];
|
|
@@ -50,6 +50,7 @@ function createChartContextChart(chart) {
|
|
|
50
50
|
setTimeBucket: chart.setTimeBucket,
|
|
51
51
|
availableTimeBuckets: chart.availableTimeBuckets,
|
|
52
52
|
isTimeSeries: chart.isTimeSeries,
|
|
53
|
+
connectNulls: chart.connectNulls,
|
|
53
54
|
filters: new Map(chart.filters),
|
|
54
55
|
toggleFilter: (columnId, value) => {
|
|
55
56
|
if (!isKnownColumnId(columnIds, columnId)) throw new Error(`Unknown chart column ID: "${columnId}"`);
|
|
@@ -70,6 +71,8 @@ function createChartContextChart(chart) {
|
|
|
70
71
|
chart.setReferenceDateId(columnId);
|
|
71
72
|
},
|
|
72
73
|
availableDateColumns: chart.availableDateColumns,
|
|
74
|
+
dateRangePreset: chart.dateRangePreset,
|
|
75
|
+
setDateRangePreset: chart.setDateRangePreset,
|
|
73
76
|
dateRangeFilter: chart.dateRangeFilter,
|
|
74
77
|
setDateRangeFilter: chart.setDateRangeFilter,
|
|
75
78
|
transformedData: chart.transformedData,
|
|
@@ -22,9 +22,9 @@ function formatDate(date) {
|
|
|
22
22
|
* Renders nothing if no date columns are available.
|
|
23
23
|
*/
|
|
24
24
|
function ChartDateRangeBadge({ className }) {
|
|
25
|
-
const { dateRange,
|
|
25
|
+
const { dateRange, dateRangePreset, availableDateColumns } = useChartContext();
|
|
26
26
|
if (availableDateColumns.length === 0) return null;
|
|
27
|
-
const activeLabel = resolvePresetLabel(
|
|
27
|
+
const activeLabel = resolvePresetLabel(dateRangePreset);
|
|
28
28
|
const hasRange = dateRange?.min && dateRange?.max;
|
|
29
29
|
return /* @__PURE__ */ jsxs("div", {
|
|
30
30
|
className: `inline-flex h-7 items-center gap-1.5 rounded-lg border border-border/50 bg-muted/30 px-2.5 text-xs text-muted-foreground ${className ?? ""}`,
|
|
@@ -1,86 +1,14 @@
|
|
|
1
|
+
import { DATE_RANGE_PRESETS, getPresetLabel } from "../core/date-range-presets.mjs";
|
|
1
2
|
import { useChartContext } from "./chart-context.mjs";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/ui/chart-date-range-panel.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Date range panel content — reusable by both ChartDateRange (inside a popover)
|
|
7
7
|
* and ChartToolbarOverflow (rendered inline).
|
|
8
8
|
*
|
|
9
|
-
* Shows preset buttons (All time, Last 7 days, etc.), a reference date
|
|
9
|
+
* Shows preset buttons (Auto, All time, Last 7 days, etc.), a reference date
|
|
10
10
|
* column picker, and custom date inputs.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* Build the list of date range presets relative to "now".
|
|
14
|
-
*
|
|
15
|
-
* "All time" → null (no date filtering)
|
|
16
|
-
* Other presets → { from: Date, to: null } (bounded filter)
|
|
17
|
-
*/
|
|
18
|
-
function getPresets() {
|
|
19
|
-
return [
|
|
20
|
-
{
|
|
21
|
-
label: "All time",
|
|
22
|
-
buildFilter: () => null
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: "Last 7 days",
|
|
26
|
-
buildFilter: () => ({
|
|
27
|
-
from: daysAgo(7),
|
|
28
|
-
to: null
|
|
29
|
-
})
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
label: "Last 30 days",
|
|
33
|
-
buildFilter: () => ({
|
|
34
|
-
from: daysAgo(30),
|
|
35
|
-
to: null
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: "Last 3 months",
|
|
40
|
-
buildFilter: () => ({
|
|
41
|
-
from: monthsAgo(3),
|
|
42
|
-
to: null
|
|
43
|
-
})
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
label: "Last 6 months",
|
|
47
|
-
buildFilter: () => ({
|
|
48
|
-
from: monthsAgo(6),
|
|
49
|
-
to: null
|
|
50
|
-
})
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
label: "Last 12 months",
|
|
54
|
-
buildFilter: () => ({
|
|
55
|
-
from: monthsAgo(12),
|
|
56
|
-
to: null
|
|
57
|
-
})
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
label: "Year to date",
|
|
61
|
-
buildFilter: () => ({
|
|
62
|
-
from: startOfYear(),
|
|
63
|
-
to: null
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
function daysAgo(n) {
|
|
69
|
-
const d = /* @__PURE__ */ new Date();
|
|
70
|
-
d.setDate(d.getDate() - n);
|
|
71
|
-
d.setHours(0, 0, 0, 0);
|
|
72
|
-
return d;
|
|
73
|
-
}
|
|
74
|
-
function monthsAgo(n) {
|
|
75
|
-
const d = /* @__PURE__ */ new Date();
|
|
76
|
-
d.setMonth(d.getMonth() - n);
|
|
77
|
-
d.setHours(0, 0, 0, 0);
|
|
78
|
-
return d;
|
|
79
|
-
}
|
|
80
|
-
function startOfYear() {
|
|
81
|
-
const d = /* @__PURE__ */ new Date();
|
|
82
|
-
return new Date(d.getFullYear(), 0, 1);
|
|
83
|
-
}
|
|
84
12
|
/** Format a Date as YYYY-MM-DD for native date input value. */
|
|
85
13
|
function toInputValue(date) {
|
|
86
14
|
if (!date) return "";
|
|
@@ -93,24 +21,14 @@ function fromInputValue(value) {
|
|
|
93
21
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
94
22
|
}
|
|
95
23
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
24
|
+
* Resolve the display label for the current date range state.
|
|
25
|
+
*
|
|
26
|
+
* When a preset is active, returns the preset label.
|
|
27
|
+
* When no preset is active (custom range), returns "Custom".
|
|
99
28
|
*/
|
|
100
|
-
function resolvePresetLabel(
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
for (const preset of presets) {
|
|
104
|
-
const pf = preset.buildFilter();
|
|
105
|
-
if (pf === null) continue;
|
|
106
|
-
if (sameDay(pf.from, filter.from) && sameDay(pf.to, filter.to)) return preset.label;
|
|
107
|
-
}
|
|
108
|
-
return "Custom";
|
|
109
|
-
}
|
|
110
|
-
function sameDay(a, b) {
|
|
111
|
-
if (!a && !b) return true;
|
|
112
|
-
if (!a || !b) return false;
|
|
113
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
29
|
+
function resolvePresetLabel(dateRangePreset) {
|
|
30
|
+
if (dateRangePreset === null) return "Custom";
|
|
31
|
+
return getPresetLabel(dateRangePreset);
|
|
114
32
|
}
|
|
115
33
|
/**
|
|
116
34
|
* Date range panel content (no popover wrapper).
|
|
@@ -119,13 +37,11 @@ function sameDay(a, b) {
|
|
|
119
37
|
* @property className - Additional CSS classes
|
|
120
38
|
*/
|
|
121
39
|
function ChartDateRangePanel({ onClose, className }) {
|
|
122
|
-
const { dateRangeFilter, setDateRangeFilter, referenceDateId, setReferenceDateId, availableDateColumns } = useChartContext();
|
|
123
|
-
const presets = useMemo(() => getPresets(), []);
|
|
124
|
-
const activeLabel = resolvePresetLabel(dateRangeFilter);
|
|
40
|
+
const { dateRangePreset, setDateRangePreset, dateRangeFilter, setDateRangeFilter, referenceDateId, setReferenceDateId, availableDateColumns } = useChartContext();
|
|
125
41
|
const hasMultipleDateColumns = availableDateColumns.length > 1;
|
|
126
|
-
const handlePreset = (
|
|
127
|
-
|
|
128
|
-
|
|
42
|
+
const handlePreset = (presetId) => {
|
|
43
|
+
setDateRangePreset(presetId);
|
|
44
|
+
onClose?.();
|
|
129
45
|
};
|
|
130
46
|
const handleCustomFrom = (value) => {
|
|
131
47
|
setDateRangeFilter({
|
|
@@ -166,12 +82,14 @@ function ChartDateRangePanel({ onClose, className }) {
|
|
|
166
82
|
children: "Range"
|
|
167
83
|
}), /* @__PURE__ */ jsx("div", {
|
|
168
84
|
className: "grid grid-cols-2 gap-1",
|
|
169
|
-
children:
|
|
85
|
+
children: DATE_RANGE_PRESETS.map((preset) => {
|
|
86
|
+
const isActive = dateRangePreset === preset.id;
|
|
170
87
|
return /* @__PURE__ */ jsx("button", {
|
|
171
|
-
onClick: () => handlePreset(preset),
|
|
172
|
-
|
|
88
|
+
onClick: () => handlePreset(preset.id),
|
|
89
|
+
title: preset.description,
|
|
90
|
+
className: `rounded-md px-2 py-1.5 text-left text-xs transition-colors ${isActive ? "bg-primary/10 font-medium text-primary" : "text-foreground hover:bg-muted"}`,
|
|
173
91
|
children: preset.label
|
|
174
|
-
}, preset.
|
|
92
|
+
}, preset.id);
|
|
175
93
|
})
|
|
176
94
|
})]
|
|
177
95
|
}),
|
|
@@ -24,12 +24,12 @@ function formatDate(date) {
|
|
|
24
24
|
* Also serves as the reference date column picker when multiple date columns exist.
|
|
25
25
|
*/
|
|
26
26
|
function ChartDateRange({ className }) {
|
|
27
|
-
const { dateRange,
|
|
27
|
+
const { dateRange, dateRangePreset, availableDateColumns } = useChartContext();
|
|
28
28
|
const [isOpen, setIsOpen] = useState(false);
|
|
29
29
|
const triggerRef = useRef(null);
|
|
30
30
|
if (availableDateColumns.length === 0) return null;
|
|
31
|
-
const activeLabel = resolvePresetLabel(
|
|
32
|
-
const isFiltered =
|
|
31
|
+
const activeLabel = resolvePresetLabel(dateRangePreset);
|
|
32
|
+
const isFiltered = dateRangePreset !== "all-time";
|
|
33
33
|
const hasRange = dateRange?.min && dateRange?.max;
|
|
34
34
|
return /* @__PURE__ */ jsxs("div", {
|
|
35
35
|
className,
|
|
@@ -6,9 +6,11 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
/** Custom dropdown to select the groupBy column. */
|
|
8
8
|
declare function ChartGroupBySelector({
|
|
9
|
-
className
|
|
9
|
+
className,
|
|
10
|
+
hideIcon
|
|
10
11
|
}: {
|
|
11
12
|
className?: string;
|
|
13
|
+
hideIcon?: boolean;
|
|
12
14
|
}): react_jsx_runtime0.JSX.Element | null;
|
|
13
15
|
//#endregion
|
|
14
16
|
export { ChartGroupBySelector };
|
|
@@ -2,12 +2,13 @@ import { CHART_TYPE_CONFIG } from "../core/chart-capabilities.mjs";
|
|
|
2
2
|
import { useChartContext } from "./chart-context.mjs";
|
|
3
3
|
import { ChartSelect } from "./chart-select.mjs";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { Layers } from "lucide-react";
|
|
5
6
|
//#region src/ui/chart-group-by-selector.tsx
|
|
6
7
|
/**
|
|
7
8
|
* GroupBy selector — premium custom dropdown replacing native <select>.
|
|
8
9
|
*/
|
|
9
10
|
/** Custom dropdown to select the groupBy column. */
|
|
10
|
-
function ChartGroupBySelector({ className }) {
|
|
11
|
+
function ChartGroupBySelector({ className, hideIcon }) {
|
|
11
12
|
const { chartType, groupById, setGroupBy, availableGroupBys } = useChartContext();
|
|
12
13
|
if (!CHART_TYPE_CONFIG[chartType].supportsGrouping || availableGroupBys.length === 0) return null;
|
|
13
14
|
const options = [{
|
|
@@ -22,6 +23,8 @@ function ChartGroupBySelector({ className }) {
|
|
|
22
23
|
options,
|
|
23
24
|
onChange: (v) => setGroupBy(v || null),
|
|
24
25
|
ariaLabel: "Group by",
|
|
26
|
+
icon: Layers,
|
|
27
|
+
hideIcon,
|
|
25
28
|
className
|
|
26
29
|
});
|
|
27
30
|
}
|