@trackunit/react-chart-components 2.1.41 → 2.1.42-alpha-7bce9c1cf2a.0
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/index.cjs.js +137 -243
- package/index.esm.js +138 -243
- package/package.json +7 -7
- package/src/BarChart/BarChart.d.ts +1 -136
- package/src/index.d.ts +2 -3
- package/src/utils/useChartNumberFormat.d.ts +0 -16
package/index.cjs.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
-
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
5
|
-
var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
|
|
6
|
-
var reactDateAndTimeHooks = require('@trackunit/react-date-and-time-hooks');
|
|
7
|
-
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
8
|
-
var react = require('react');
|
|
9
4
|
var reactComponents = require('@trackunit/react-components');
|
|
5
|
+
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
10
6
|
var sharedUtils = require('@trackunit/shared-utils');
|
|
11
7
|
var echarts = require('echarts');
|
|
8
|
+
var react = require('react');
|
|
9
|
+
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
10
|
+
var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
|
|
11
|
+
var reactDateAndTimeHooks = require('@trackunit/react-date-and-time-hooks');
|
|
12
12
|
|
|
13
13
|
function _interopNamespaceDefault(e) {
|
|
14
14
|
var n = Object.create(null);
|
|
@@ -303,241 +303,6 @@ const useChartColor = () => {
|
|
|
303
303
|
}), [chartColor, chartColorArray, chartStatusColor]);
|
|
304
304
|
};
|
|
305
305
|
|
|
306
|
-
/**
|
|
307
|
-
* Locale-aware number formatting helpers for charts. Mirrors `useChartColor`:
|
|
308
|
-
* a hook that returns memoized formatting functions so chart consumers share one
|
|
309
|
-
* consistent compact-number style instead of hand-rolling `>= 1000 → k` logic.
|
|
310
|
-
*
|
|
311
|
-
* @returns {{ formatCompact: (value: number) => string }} An object exposing
|
|
312
|
-
* `formatCompact`, a locale-aware compact formatter (e.g. `40000 → "40K"`, `40_000_000 → "40M"`).
|
|
313
|
-
* @example Abbreviate a y-axis value
|
|
314
|
-
* ```tsx
|
|
315
|
-
* const { formatCompact } = useChartNumberFormat();
|
|
316
|
-
* formatCompact(40000); // "40K"
|
|
317
|
-
* ```
|
|
318
|
-
*/
|
|
319
|
-
const useChartNumberFormat = () => {
|
|
320
|
-
const locale = reactDateAndTimeHooks.useLocale();
|
|
321
|
-
const formatCompact = react.useCallback((value) => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }).format(value), [locale]);
|
|
322
|
-
return react.useMemo(() => ({ formatCompact }), [formatCompact]);
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const DEFAULT_DATE_FORMAT = { selectFormat: "dateOnly", dateFormat: "medium" };
|
|
326
|
-
const DEFAULT_ROTATE_LABELS = 45;
|
|
327
|
-
const isDateData = (data) => !!data && "date" in data;
|
|
328
|
-
const isEChartsTooltipParam = (param) => typeof param === "object" &&
|
|
329
|
-
param !== null &&
|
|
330
|
-
"seriesName" in param &&
|
|
331
|
-
typeof param.seriesName === "string" &&
|
|
332
|
-
"seriesIndex" in param &&
|
|
333
|
-
typeof param.seriesIndex === "number" &&
|
|
334
|
-
"dataIndex" in param &&
|
|
335
|
-
typeof param.dataIndex === "number" &&
|
|
336
|
-
"color" in param &&
|
|
337
|
-
typeof param.color === "string" &&
|
|
338
|
-
"marker" in param &&
|
|
339
|
-
typeof param.marker === "string";
|
|
340
|
-
/**
|
|
341
|
-
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
342
|
-
* with sensible defaults for displaying categorical or time-series data.
|
|
343
|
-
*
|
|
344
|
-
* All customization props are optional and default to the component's original rendering, so
|
|
345
|
-
* existing usages are unaffected.
|
|
346
|
-
*
|
|
347
|
-
* ### When to use
|
|
348
|
-
* - To compare values across categories
|
|
349
|
-
* - To show trends over time with discrete intervals
|
|
350
|
-
* - When you need multiple series displayed side by side
|
|
351
|
-
*
|
|
352
|
-
* @example Bar chart with date-based data
|
|
353
|
-
* ```tsx
|
|
354
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
355
|
-
*
|
|
356
|
-
* const UtilizationChart = () => (
|
|
357
|
-
* <BarChart
|
|
358
|
-
* series={{
|
|
359
|
-
* name: "Utilization",
|
|
360
|
-
* data: [
|
|
361
|
-
* { date: "2024-01-01", value: 85 },
|
|
362
|
-
* { date: "2024-01-02", value: 72 },
|
|
363
|
-
* { date: "2024-01-03", value: 91 },
|
|
364
|
-
* ],
|
|
365
|
-
* }}
|
|
366
|
-
* units="%"
|
|
367
|
-
* onClick={(event) => console.log("Clicked bar:", event.data)}
|
|
368
|
-
* />
|
|
369
|
-
* );
|
|
370
|
-
* ```
|
|
371
|
-
* @example Multi-series bar chart with data zoom
|
|
372
|
-
* ```tsx
|
|
373
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
374
|
-
*
|
|
375
|
-
* const ComparisonChart = () => (
|
|
376
|
-
* <BarChart
|
|
377
|
-
* series={[
|
|
378
|
-
* { name: "2023", color: "#3b82f6", data: [{ key: "Q1", value: 100 }, { key: "Q2", value: 120 }] },
|
|
379
|
-
* { name: "2024", color: "#10b981", data: [{ key: "Q1", value: 130 }, { key: "Q2", value: 145 }] },
|
|
380
|
-
* ]}
|
|
381
|
-
* units="units"
|
|
382
|
-
* showDataZoom={true}
|
|
383
|
-
* />
|
|
384
|
-
* );
|
|
385
|
-
* ```
|
|
386
|
-
* @example Abbreviated y-axis, horizontal x-axis labels and a hidden legend
|
|
387
|
-
* ```tsx
|
|
388
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
389
|
-
*
|
|
390
|
-
* const EmissionsChart = () => (
|
|
391
|
-
* <BarChart
|
|
392
|
-
* series={{ name: "Emissions", data: [{ key: "Excavator", value: 40000, color: "#3b82f6" }] }}
|
|
393
|
-
* units="t"
|
|
394
|
-
* yAxis={{ abbreviate: true }}
|
|
395
|
-
* xAxis={{ rotateLabels: 0, formatLabel: (value) => value.slice(0, 10) }}
|
|
396
|
-
* showLegend={false}
|
|
397
|
-
* />
|
|
398
|
-
* );
|
|
399
|
-
* ```
|
|
400
|
-
* @example Custom tooltip from the typed payload
|
|
401
|
-
* ```tsx
|
|
402
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
403
|
-
*
|
|
404
|
-
* const TrendChart = () => (
|
|
405
|
-
* <BarChart
|
|
406
|
-
* series={{ name: "Emissions", data: [{ date: "2024-01-01", value: 120 }] }}
|
|
407
|
-
* tooltip={{
|
|
408
|
-
* trigger: "axis",
|
|
409
|
-
* formatter: ({ axisValue, entries }) =>
|
|
410
|
-
* `${axisValue}<br/>${entries.map((e) => `${e.marker} ${e.seriesName}: ${e.value ?? 0}`).join("<br/>")}`,
|
|
411
|
-
* }}
|
|
412
|
-
* />
|
|
413
|
-
* );
|
|
414
|
-
* ```
|
|
415
|
-
* @param {BarChartProps} props - The props for the Chart component
|
|
416
|
-
* @returns {ReactElement} Chart component
|
|
417
|
-
*/
|
|
418
|
-
const BarChart = ({ series, loading = false, onClick, className, style, "data-testid": dataTestId, units, showDataZoom = false, xAxis, yAxis, tooltip, showLegend = true, ref, }) => {
|
|
419
|
-
const { formatDate } = reactDateAndTimeHooks.useDateAndTime();
|
|
420
|
-
const { chartColor } = useChartColor();
|
|
421
|
-
const { formatCompact } = useChartNumberFormat();
|
|
422
|
-
const seriesData = react.useMemo(() => {
|
|
423
|
-
const seriesAsArray = Array.isArray(series) ? series : series !== undefined ? [series] : [];
|
|
424
|
-
return seriesAsArray.map((s, i) => ({
|
|
425
|
-
...s,
|
|
426
|
-
color: s.color || chartColor(i),
|
|
427
|
-
type: "bar",
|
|
428
|
-
cursor: "arrow",
|
|
429
|
-
barGap: "0",
|
|
430
|
-
data: s.data.map(d => ({
|
|
431
|
-
...d,
|
|
432
|
-
itemStyle: d.color ? { color: d.color } : undefined,
|
|
433
|
-
})),
|
|
434
|
-
}));
|
|
435
|
-
}, [series, chartColor]);
|
|
436
|
-
const yAxisLabelFormatter = react.useMemo(() => {
|
|
437
|
-
if (yAxis?.formatLabel) {
|
|
438
|
-
return yAxis.formatLabel;
|
|
439
|
-
}
|
|
440
|
-
if (yAxis?.abbreviate) {
|
|
441
|
-
return (value) => formatCompact(value);
|
|
442
|
-
}
|
|
443
|
-
return undefined;
|
|
444
|
-
}, [yAxis, formatCompact]);
|
|
445
|
-
const buildTooltipParams = react.useCallback((rawParams) => {
|
|
446
|
-
const params = Array.isArray(rawParams) ? rawParams : [rawParams];
|
|
447
|
-
const validParams = params.filter(isEChartsTooltipParam);
|
|
448
|
-
const firstParam = validParams[0];
|
|
449
|
-
const axisValueRaw = firstParam?.axisValue ?? firstParam?.name;
|
|
450
|
-
const axisValue = axisValueRaw === undefined ? "" : String(axisValueRaw);
|
|
451
|
-
const entries = validParams.map(param => {
|
|
452
|
-
const dataItem = seriesData[param.seriesIndex]?.data[param.dataIndex];
|
|
453
|
-
return {
|
|
454
|
-
seriesName: param.seriesName,
|
|
455
|
-
value: dataItem?.value,
|
|
456
|
-
color: param.color,
|
|
457
|
-
marker: param.marker,
|
|
458
|
-
dataIndex: param.dataIndex,
|
|
459
|
-
original: dataItem?.original,
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
return { axisValue, entries };
|
|
463
|
-
}, [seriesData]);
|
|
464
|
-
const tooltipTrigger = tooltip?.trigger ?? "item";
|
|
465
|
-
const tooltipFormatter = tooltip?.formatter;
|
|
466
|
-
const tooltipValueFormatter = tooltip?.formatValue;
|
|
467
|
-
return (jsxRuntime.jsxs("div", { className: cvaChartRoot$1({ className }), "data-testid": dataTestId, ref: ref, style: style, children: [showLegend ? (jsxRuntime.jsx("div", { className: "flex-0 flex flex-row gap-2 place-self-end pr-8", "data-testid": "legend", children: seriesData.map((item, index) => (jsxRuntime.jsx(LegendItem, { color: item.color || chartColor(index), "data-testid": `legend-${item.name}`, label: item.name }, item.name))) })) : null, jsxRuntime.jsx("div", { className: "flex w-full flex-1", children: jsxRuntime.jsx(Chart, { "data-testid": "bar-chart", onClick: onClick
|
|
468
|
-
? (e) => {
|
|
469
|
-
onClick(e);
|
|
470
|
-
}
|
|
471
|
-
: undefined, options: {
|
|
472
|
-
tooltip: {
|
|
473
|
-
trigger: tooltipTrigger,
|
|
474
|
-
confine: true,
|
|
475
|
-
axisPointer: tooltipTrigger === "axis" ? { type: "shadow" } : undefined,
|
|
476
|
-
formatter: tooltipFormatter
|
|
477
|
-
? (rawParams) => tooltipFormatter(buildTooltipParams(rawParams))
|
|
478
|
-
: undefined,
|
|
479
|
-
valueFormatter: tooltipFormatter
|
|
480
|
-
? undefined
|
|
481
|
-
: value => tooltipValueFormatter && typeof value === "number"
|
|
482
|
-
? tooltipValueFormatter(value)
|
|
483
|
-
: `${value} ${units ?? ""}`,
|
|
484
|
-
},
|
|
485
|
-
grid: {
|
|
486
|
-
left: "2%",
|
|
487
|
-
right: "2%",
|
|
488
|
-
bottom: "10%",
|
|
489
|
-
top: "10%",
|
|
490
|
-
containLabel: true,
|
|
491
|
-
},
|
|
492
|
-
xAxis: [
|
|
493
|
-
{
|
|
494
|
-
type: "category",
|
|
495
|
-
axisLabel: {
|
|
496
|
-
formatter: (value, index) => {
|
|
497
|
-
if (xAxis?.formatLabel) {
|
|
498
|
-
return xAxis.formatLabel(value, index);
|
|
499
|
-
}
|
|
500
|
-
if (isDateData(seriesData[0]?.data[0])) {
|
|
501
|
-
const date = formatDate(dateAndTimeUtils.toDateUtil(value), xAxis?.dateFormat ?? DEFAULT_DATE_FORMAT);
|
|
502
|
-
return date.replace(",", ",\n");
|
|
503
|
-
}
|
|
504
|
-
return value;
|
|
505
|
-
},
|
|
506
|
-
fontSize: 10,
|
|
507
|
-
fontFamily: uiDesignTokens.fontFamily,
|
|
508
|
-
rotate: xAxis?.rotateLabels ?? DEFAULT_ROTATE_LABELS,
|
|
509
|
-
overflow: "truncate",
|
|
510
|
-
},
|
|
511
|
-
data: seriesData[0]?.data.map(data => (isDateData(data) ? data.date : data.key)),
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
dataZoom: showDataZoom
|
|
515
|
-
? [
|
|
516
|
-
{
|
|
517
|
-
type: "slider",
|
|
518
|
-
id: "insideX",
|
|
519
|
-
xAxisIndex: 0,
|
|
520
|
-
startValue: 0,
|
|
521
|
-
endValue: (seriesData[0]?.data.length ?? 0) > 10 ? 10 : (seriesData[0]?.data.length ?? 0),
|
|
522
|
-
showDetail: false,
|
|
523
|
-
},
|
|
524
|
-
]
|
|
525
|
-
: [],
|
|
526
|
-
yAxis: [
|
|
527
|
-
{
|
|
528
|
-
type: "value",
|
|
529
|
-
axisLabel: {
|
|
530
|
-
fontSize: 10,
|
|
531
|
-
fontFamily: uiDesignTokens.fontFamily,
|
|
532
|
-
formatter: yAxisLabelFormatter,
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
],
|
|
536
|
-
series: seriesData,
|
|
537
|
-
}, showLoading: loading, style: { height: "100%", minHeight: "150px", width: "100%" } }) })] }));
|
|
538
|
-
};
|
|
539
|
-
const cvaChartRoot$1 = cssClassVarianceUtilities.cvaMerge(["flex", "flex-col", "items-center", "h-full"]);
|
|
540
|
-
|
|
541
306
|
/**
|
|
542
307
|
* Limits the data set to the given limit.
|
|
543
308
|
* If the data set is larger than the limit, the data set is limited to the limit and the rest of the data is added to the "Others" group.
|
|
@@ -743,9 +508,9 @@ const DonutChart = ({ data, size = "full", loading = false, onClick, className,
|
|
|
743
508
|
if (loading) {
|
|
744
509
|
return jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", "data-testid": dataTestId ? `${dataTestId}-loading` : "donut-chart-loading" });
|
|
745
510
|
}
|
|
746
|
-
return (jsxRuntime.jsxs("div", { className: cvaChartRoot({ className }), "data-testid": dataTestId, ref: mergedRef, style: style, children: [jsxRuntime.jsx("div", { className: cvaChartContainer(), children: jsxRuntime.jsx(Chart, { className: cvaChart({ size }), "data-testid": dataTestId ? `chart-${dataTestId}` : undefined, onChartReady: handleChartReady, onClick: handleChartClick, onEvents: handleChartEvents, options: chartOptions, style: { width: "100%", height: "100%" } }) }), size === "full" && (jsxRuntime.jsx("div", { className: cvaLegend(), "data-testid": "legend", children: limitedData.map((item, index) => (jsxRuntime.jsx(LegendItem, { className: "p-1.5 py-0.5", color: item.color ?? chartColor(index), count: item.value, "data-testid": `legend-${item.id}`, disabled: (item.value ?? 0) === 0, hideValue: hideLegendValues, label: item.name, onClick: onClick ? () => onClick(item) : undefined, onMouseEnter: () => handleLegendMouseEnter(item), onMouseLeave: handleLegendMouseLeave, selected: item.selected, unit: unit }, item.id))) }))] }));
|
|
511
|
+
return (jsxRuntime.jsxs("div", { className: cvaChartRoot$1({ className }), "data-testid": dataTestId, ref: mergedRef, style: style, children: [jsxRuntime.jsx("div", { className: cvaChartContainer(), children: jsxRuntime.jsx(Chart, { className: cvaChart({ size }), "data-testid": dataTestId ? `chart-${dataTestId}` : undefined, onChartReady: handleChartReady, onClick: handleChartClick, onEvents: handleChartEvents, options: chartOptions, style: { width: "100%", height: "100%" } }) }), size === "full" && (jsxRuntime.jsx("div", { className: cvaLegend(), "data-testid": "legend", children: limitedData.map((item, index) => (jsxRuntime.jsx(LegendItem, { className: "p-1.5 py-0.5", color: item.color ?? chartColor(index), count: item.value, "data-testid": `legend-${item.id}`, disabled: (item.value ?? 0) === 0, hideValue: hideLegendValues, label: item.name, onClick: onClick ? () => onClick(item) : undefined, onMouseEnter: () => handleLegendMouseEnter(item), onMouseLeave: handleLegendMouseLeave, selected: item.selected, unit: unit }, item.id))) }))] }));
|
|
747
512
|
};
|
|
748
|
-
const cvaChartRoot = cssClassVarianceUtilities.cvaMerge([
|
|
513
|
+
const cvaChartRoot$1 = cssClassVarianceUtilities.cvaMerge([
|
|
749
514
|
"flex",
|
|
750
515
|
"w-full",
|
|
751
516
|
"h-full",
|
|
@@ -768,10 +533,139 @@ const cvaChart = cssClassVarianceUtilities.cvaMerge(["flex-0", "max-w-[200px]",
|
|
|
768
533
|
});
|
|
769
534
|
const cvaLegend = cssClassVarianceUtilities.cvaMerge(["flex", "overflow-auto", "justify-start", "flex-col", "flex-1"]);
|
|
770
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
538
|
+
* with sensible defaults for displaying categorical or time-series data.
|
|
539
|
+
*
|
|
540
|
+
* ### When to use
|
|
541
|
+
* - To compare values across categories
|
|
542
|
+
* - To show trends over time with discrete intervals
|
|
543
|
+
* - When you need multiple series displayed side by side
|
|
544
|
+
*
|
|
545
|
+
* @example Bar chart with date-based data
|
|
546
|
+
* ```tsx
|
|
547
|
+
* import { BarChart } from "@trackunit/react-chart-components";
|
|
548
|
+
*
|
|
549
|
+
* const UtilizationChart = () => (
|
|
550
|
+
* <BarChart
|
|
551
|
+
* series={{
|
|
552
|
+
* name: "Utilization",
|
|
553
|
+
* data: [
|
|
554
|
+
* { date: "2024-01-01", value: 85 },
|
|
555
|
+
* { date: "2024-01-02", value: 72 },
|
|
556
|
+
* { date: "2024-01-03", value: 91 },
|
|
557
|
+
* ],
|
|
558
|
+
* }}
|
|
559
|
+
* units="%"
|
|
560
|
+
* onClick={(event) => console.log("Clicked bar:", event.data)}
|
|
561
|
+
* />
|
|
562
|
+
* );
|
|
563
|
+
* ```
|
|
564
|
+
* @example Multi-series bar chart with data zoom
|
|
565
|
+
* ```tsx
|
|
566
|
+
* import { BarChart } from "@trackunit/react-chart-components";
|
|
567
|
+
*
|
|
568
|
+
* const ComparisonChart = () => (
|
|
569
|
+
* <BarChart
|
|
570
|
+
* series={[
|
|
571
|
+
* { name: "2023", color: "#3b82f6", data: [{ key: "Q1", value: 100 }, { key: "Q2", value: 120 }] },
|
|
572
|
+
* { name: "2024", color: "#10b981", data: [{ key: "Q1", value: 130 }, { key: "Q2", value: 145 }] },
|
|
573
|
+
* ]}
|
|
574
|
+
* units="units"
|
|
575
|
+
* showDataZoom={true}
|
|
576
|
+
* />
|
|
577
|
+
* );
|
|
578
|
+
* ```
|
|
579
|
+
* @param {BarChartProps} props - The props for the Chart component
|
|
580
|
+
* @returns {ReactElement} Chart component
|
|
581
|
+
*/
|
|
582
|
+
const BarChart = ({ series, loading = false, onClick, className, style, "data-testid": dataTestId, units, showDataZoom = false, ref, }) => {
|
|
583
|
+
const { formatDate } = reactDateAndTimeHooks.useDateAndTime();
|
|
584
|
+
const { chartColor } = useChartColor();
|
|
585
|
+
const seriesData = react.useMemo(() => {
|
|
586
|
+
const seriesAsArray = Array.isArray(series) ? series : series !== undefined ? [series] : [];
|
|
587
|
+
return seriesAsArray.map((s, i) => ({
|
|
588
|
+
...s,
|
|
589
|
+
color: s.color || chartColor(i),
|
|
590
|
+
type: "bar",
|
|
591
|
+
cursor: "arrow",
|
|
592
|
+
barGap: "0",
|
|
593
|
+
data: s.data.map(d => ({
|
|
594
|
+
...d,
|
|
595
|
+
})),
|
|
596
|
+
}));
|
|
597
|
+
}, [series, chartColor]);
|
|
598
|
+
const isDateData = (data) => !!data && "date" in data;
|
|
599
|
+
return (jsxRuntime.jsxs("div", { className: cvaChartRoot({ className }), "data-testid": dataTestId, ref: ref, style: style, children: [jsxRuntime.jsx("div", { className: "flex-0 flex flex-row gap-2 place-self-end pr-8", "data-testid": "legend", children: seriesData.map((item, index) => (jsxRuntime.jsx(LegendItem, { color: item.color || chartColor(index), "data-testid": `legend-${item.name}`, label: item.name }, item.name))) }), jsxRuntime.jsx("div", { className: "flex w-full flex-1", children: jsxRuntime.jsx(Chart, { "data-testid": "bar-chart", onClick: onClick
|
|
600
|
+
? (e) => {
|
|
601
|
+
onClick(e);
|
|
602
|
+
}
|
|
603
|
+
: undefined, options: {
|
|
604
|
+
tooltip: {
|
|
605
|
+
trigger: "item",
|
|
606
|
+
confine: true,
|
|
607
|
+
valueFormatter: value => `${value} ${units ?? ""}`,
|
|
608
|
+
},
|
|
609
|
+
grid: {
|
|
610
|
+
left: "2%",
|
|
611
|
+
right: "2%",
|
|
612
|
+
bottom: "10%",
|
|
613
|
+
top: "10%",
|
|
614
|
+
containLabel: true,
|
|
615
|
+
},
|
|
616
|
+
xAxis: [
|
|
617
|
+
{
|
|
618
|
+
type: "category",
|
|
619
|
+
axisLabel: {
|
|
620
|
+
formatter: value => {
|
|
621
|
+
if (isDateData(seriesData[0]?.data[0])) {
|
|
622
|
+
const date = formatDate(dateAndTimeUtils.toDateUtil(value), {
|
|
623
|
+
selectFormat: "dateOnly",
|
|
624
|
+
dateFormat: "medium",
|
|
625
|
+
});
|
|
626
|
+
return date.replace(",", ",\n");
|
|
627
|
+
}
|
|
628
|
+
return value;
|
|
629
|
+
},
|
|
630
|
+
fontSize: 10,
|
|
631
|
+
fontFamily: uiDesignTokens.fontFamily,
|
|
632
|
+
rotate: 45,
|
|
633
|
+
overflow: "truncate",
|
|
634
|
+
},
|
|
635
|
+
data: seriesData[0]?.data
|
|
636
|
+
.filter(({ value }) => Boolean(value))
|
|
637
|
+
.map(data => (isDateData(data) ? data.date : data.key)),
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
dataZoom: showDataZoom
|
|
641
|
+
? [
|
|
642
|
+
{
|
|
643
|
+
type: "slider",
|
|
644
|
+
id: "insideX",
|
|
645
|
+
xAxisIndex: 0,
|
|
646
|
+
startValue: 0,
|
|
647
|
+
endValue: (seriesData[0]?.data.length ?? 0) > 10 ? 10 : (seriesData[0]?.data.length ?? 0),
|
|
648
|
+
showDetail: false,
|
|
649
|
+
},
|
|
650
|
+
]
|
|
651
|
+
: [],
|
|
652
|
+
yAxis: [
|
|
653
|
+
{
|
|
654
|
+
type: "value",
|
|
655
|
+
axisLabel: {
|
|
656
|
+
fontSize: 10,
|
|
657
|
+
fontFamily: uiDesignTokens.fontFamily,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
series: seriesData,
|
|
662
|
+
}, showLoading: loading, style: { height: "100%", minHeight: "150px", width: "100%" } }) })] }));
|
|
663
|
+
};
|
|
664
|
+
const cvaChartRoot = cssClassVarianceUtilities.cvaMerge(["flex", "flex-col", "items-center", "h-full"]);
|
|
665
|
+
|
|
771
666
|
exports.BarChart = BarChart;
|
|
772
667
|
exports.Chart = Chart;
|
|
773
668
|
exports.DonutChart = DonutChart;
|
|
774
669
|
exports.EChart = EChart;
|
|
775
670
|
exports.LegendItem = LegendItem;
|
|
776
671
|
exports.useChartColor = useChartColor;
|
|
777
|
-
exports.useChartNumberFormat = useChartNumberFormat;
|
package/index.esm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
3
|
-
import { toDateUtil } from '@trackunit/date-and-time-utils';
|
|
4
|
-
import { useLocale, useDateAndTime } from '@trackunit/react-date-and-time-hooks';
|
|
5
|
-
import { DEFAULT_CHART_COLORS, CHART_STATUS_COLORS, fontFamily, DEFAULT_CHART_OTHER } from '@trackunit/ui-design-tokens';
|
|
6
|
-
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
2
|
import { Spinner, Text, useMergeRefs } from '@trackunit/react-components';
|
|
3
|
+
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
8
4
|
import { objectKeys } from '@trackunit/shared-utils';
|
|
9
5
|
import * as echarts from 'echarts';
|
|
6
|
+
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { DEFAULT_CHART_COLORS, CHART_STATUS_COLORS, DEFAULT_CHART_OTHER, fontFamily } from '@trackunit/ui-design-tokens';
|
|
8
|
+
import { toDateUtil } from '@trackunit/date-and-time-utils';
|
|
9
|
+
import { useDateAndTime } from '@trackunit/react-date-and-time-hooks';
|
|
10
10
|
|
|
11
11
|
function isECElementEvent(value) {
|
|
12
12
|
return (typeof value === "object" &&
|
|
@@ -282,241 +282,6 @@ const useChartColor = () => {
|
|
|
282
282
|
}), [chartColor, chartColorArray, chartStatusColor]);
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
-
/**
|
|
286
|
-
* Locale-aware number formatting helpers for charts. Mirrors `useChartColor`:
|
|
287
|
-
* a hook that returns memoized formatting functions so chart consumers share one
|
|
288
|
-
* consistent compact-number style instead of hand-rolling `>= 1000 → k` logic.
|
|
289
|
-
*
|
|
290
|
-
* @returns {{ formatCompact: (value: number) => string }} An object exposing
|
|
291
|
-
* `formatCompact`, a locale-aware compact formatter (e.g. `40000 → "40K"`, `40_000_000 → "40M"`).
|
|
292
|
-
* @example Abbreviate a y-axis value
|
|
293
|
-
* ```tsx
|
|
294
|
-
* const { formatCompact } = useChartNumberFormat();
|
|
295
|
-
* formatCompact(40000); // "40K"
|
|
296
|
-
* ```
|
|
297
|
-
*/
|
|
298
|
-
const useChartNumberFormat = () => {
|
|
299
|
-
const locale = useLocale();
|
|
300
|
-
const formatCompact = useCallback((value) => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }).format(value), [locale]);
|
|
301
|
-
return useMemo(() => ({ formatCompact }), [formatCompact]);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const DEFAULT_DATE_FORMAT = { selectFormat: "dateOnly", dateFormat: "medium" };
|
|
305
|
-
const DEFAULT_ROTATE_LABELS = 45;
|
|
306
|
-
const isDateData = (data) => !!data && "date" in data;
|
|
307
|
-
const isEChartsTooltipParam = (param) => typeof param === "object" &&
|
|
308
|
-
param !== null &&
|
|
309
|
-
"seriesName" in param &&
|
|
310
|
-
typeof param.seriesName === "string" &&
|
|
311
|
-
"seriesIndex" in param &&
|
|
312
|
-
typeof param.seriesIndex === "number" &&
|
|
313
|
-
"dataIndex" in param &&
|
|
314
|
-
typeof param.dataIndex === "number" &&
|
|
315
|
-
"color" in param &&
|
|
316
|
-
typeof param.color === "string" &&
|
|
317
|
-
"marker" in param &&
|
|
318
|
-
typeof param.marker === "string";
|
|
319
|
-
/**
|
|
320
|
-
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
321
|
-
* with sensible defaults for displaying categorical or time-series data.
|
|
322
|
-
*
|
|
323
|
-
* All customization props are optional and default to the component's original rendering, so
|
|
324
|
-
* existing usages are unaffected.
|
|
325
|
-
*
|
|
326
|
-
* ### When to use
|
|
327
|
-
* - To compare values across categories
|
|
328
|
-
* - To show trends over time with discrete intervals
|
|
329
|
-
* - When you need multiple series displayed side by side
|
|
330
|
-
*
|
|
331
|
-
* @example Bar chart with date-based data
|
|
332
|
-
* ```tsx
|
|
333
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
334
|
-
*
|
|
335
|
-
* const UtilizationChart = () => (
|
|
336
|
-
* <BarChart
|
|
337
|
-
* series={{
|
|
338
|
-
* name: "Utilization",
|
|
339
|
-
* data: [
|
|
340
|
-
* { date: "2024-01-01", value: 85 },
|
|
341
|
-
* { date: "2024-01-02", value: 72 },
|
|
342
|
-
* { date: "2024-01-03", value: 91 },
|
|
343
|
-
* ],
|
|
344
|
-
* }}
|
|
345
|
-
* units="%"
|
|
346
|
-
* onClick={(event) => console.log("Clicked bar:", event.data)}
|
|
347
|
-
* />
|
|
348
|
-
* );
|
|
349
|
-
* ```
|
|
350
|
-
* @example Multi-series bar chart with data zoom
|
|
351
|
-
* ```tsx
|
|
352
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
353
|
-
*
|
|
354
|
-
* const ComparisonChart = () => (
|
|
355
|
-
* <BarChart
|
|
356
|
-
* series={[
|
|
357
|
-
* { name: "2023", color: "#3b82f6", data: [{ key: "Q1", value: 100 }, { key: "Q2", value: 120 }] },
|
|
358
|
-
* { name: "2024", color: "#10b981", data: [{ key: "Q1", value: 130 }, { key: "Q2", value: 145 }] },
|
|
359
|
-
* ]}
|
|
360
|
-
* units="units"
|
|
361
|
-
* showDataZoom={true}
|
|
362
|
-
* />
|
|
363
|
-
* );
|
|
364
|
-
* ```
|
|
365
|
-
* @example Abbreviated y-axis, horizontal x-axis labels and a hidden legend
|
|
366
|
-
* ```tsx
|
|
367
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
368
|
-
*
|
|
369
|
-
* const EmissionsChart = () => (
|
|
370
|
-
* <BarChart
|
|
371
|
-
* series={{ name: "Emissions", data: [{ key: "Excavator", value: 40000, color: "#3b82f6" }] }}
|
|
372
|
-
* units="t"
|
|
373
|
-
* yAxis={{ abbreviate: true }}
|
|
374
|
-
* xAxis={{ rotateLabels: 0, formatLabel: (value) => value.slice(0, 10) }}
|
|
375
|
-
* showLegend={false}
|
|
376
|
-
* />
|
|
377
|
-
* );
|
|
378
|
-
* ```
|
|
379
|
-
* @example Custom tooltip from the typed payload
|
|
380
|
-
* ```tsx
|
|
381
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
382
|
-
*
|
|
383
|
-
* const TrendChart = () => (
|
|
384
|
-
* <BarChart
|
|
385
|
-
* series={{ name: "Emissions", data: [{ date: "2024-01-01", value: 120 }] }}
|
|
386
|
-
* tooltip={{
|
|
387
|
-
* trigger: "axis",
|
|
388
|
-
* formatter: ({ axisValue, entries }) =>
|
|
389
|
-
* `${axisValue}<br/>${entries.map((e) => `${e.marker} ${e.seriesName}: ${e.value ?? 0}`).join("<br/>")}`,
|
|
390
|
-
* }}
|
|
391
|
-
* />
|
|
392
|
-
* );
|
|
393
|
-
* ```
|
|
394
|
-
* @param {BarChartProps} props - The props for the Chart component
|
|
395
|
-
* @returns {ReactElement} Chart component
|
|
396
|
-
*/
|
|
397
|
-
const BarChart = ({ series, loading = false, onClick, className, style, "data-testid": dataTestId, units, showDataZoom = false, xAxis, yAxis, tooltip, showLegend = true, ref, }) => {
|
|
398
|
-
const { formatDate } = useDateAndTime();
|
|
399
|
-
const { chartColor } = useChartColor();
|
|
400
|
-
const { formatCompact } = useChartNumberFormat();
|
|
401
|
-
const seriesData = useMemo(() => {
|
|
402
|
-
const seriesAsArray = Array.isArray(series) ? series : series !== undefined ? [series] : [];
|
|
403
|
-
return seriesAsArray.map((s, i) => ({
|
|
404
|
-
...s,
|
|
405
|
-
color: s.color || chartColor(i),
|
|
406
|
-
type: "bar",
|
|
407
|
-
cursor: "arrow",
|
|
408
|
-
barGap: "0",
|
|
409
|
-
data: s.data.map(d => ({
|
|
410
|
-
...d,
|
|
411
|
-
itemStyle: d.color ? { color: d.color } : undefined,
|
|
412
|
-
})),
|
|
413
|
-
}));
|
|
414
|
-
}, [series, chartColor]);
|
|
415
|
-
const yAxisLabelFormatter = useMemo(() => {
|
|
416
|
-
if (yAxis?.formatLabel) {
|
|
417
|
-
return yAxis.formatLabel;
|
|
418
|
-
}
|
|
419
|
-
if (yAxis?.abbreviate) {
|
|
420
|
-
return (value) => formatCompact(value);
|
|
421
|
-
}
|
|
422
|
-
return undefined;
|
|
423
|
-
}, [yAxis, formatCompact]);
|
|
424
|
-
const buildTooltipParams = useCallback((rawParams) => {
|
|
425
|
-
const params = Array.isArray(rawParams) ? rawParams : [rawParams];
|
|
426
|
-
const validParams = params.filter(isEChartsTooltipParam);
|
|
427
|
-
const firstParam = validParams[0];
|
|
428
|
-
const axisValueRaw = firstParam?.axisValue ?? firstParam?.name;
|
|
429
|
-
const axisValue = axisValueRaw === undefined ? "" : String(axisValueRaw);
|
|
430
|
-
const entries = validParams.map(param => {
|
|
431
|
-
const dataItem = seriesData[param.seriesIndex]?.data[param.dataIndex];
|
|
432
|
-
return {
|
|
433
|
-
seriesName: param.seriesName,
|
|
434
|
-
value: dataItem?.value,
|
|
435
|
-
color: param.color,
|
|
436
|
-
marker: param.marker,
|
|
437
|
-
dataIndex: param.dataIndex,
|
|
438
|
-
original: dataItem?.original,
|
|
439
|
-
};
|
|
440
|
-
});
|
|
441
|
-
return { axisValue, entries };
|
|
442
|
-
}, [seriesData]);
|
|
443
|
-
const tooltipTrigger = tooltip?.trigger ?? "item";
|
|
444
|
-
const tooltipFormatter = tooltip?.formatter;
|
|
445
|
-
const tooltipValueFormatter = tooltip?.formatValue;
|
|
446
|
-
return (jsxs("div", { className: cvaChartRoot$1({ className }), "data-testid": dataTestId, ref: ref, style: style, children: [showLegend ? (jsx("div", { className: "flex-0 flex flex-row gap-2 place-self-end pr-8", "data-testid": "legend", children: seriesData.map((item, index) => (jsx(LegendItem, { color: item.color || chartColor(index), "data-testid": `legend-${item.name}`, label: item.name }, item.name))) })) : null, jsx("div", { className: "flex w-full flex-1", children: jsx(Chart, { "data-testid": "bar-chart", onClick: onClick
|
|
447
|
-
? (e) => {
|
|
448
|
-
onClick(e);
|
|
449
|
-
}
|
|
450
|
-
: undefined, options: {
|
|
451
|
-
tooltip: {
|
|
452
|
-
trigger: tooltipTrigger,
|
|
453
|
-
confine: true,
|
|
454
|
-
axisPointer: tooltipTrigger === "axis" ? { type: "shadow" } : undefined,
|
|
455
|
-
formatter: tooltipFormatter
|
|
456
|
-
? (rawParams) => tooltipFormatter(buildTooltipParams(rawParams))
|
|
457
|
-
: undefined,
|
|
458
|
-
valueFormatter: tooltipFormatter
|
|
459
|
-
? undefined
|
|
460
|
-
: value => tooltipValueFormatter && typeof value === "number"
|
|
461
|
-
? tooltipValueFormatter(value)
|
|
462
|
-
: `${value} ${units ?? ""}`,
|
|
463
|
-
},
|
|
464
|
-
grid: {
|
|
465
|
-
left: "2%",
|
|
466
|
-
right: "2%",
|
|
467
|
-
bottom: "10%",
|
|
468
|
-
top: "10%",
|
|
469
|
-
containLabel: true,
|
|
470
|
-
},
|
|
471
|
-
xAxis: [
|
|
472
|
-
{
|
|
473
|
-
type: "category",
|
|
474
|
-
axisLabel: {
|
|
475
|
-
formatter: (value, index) => {
|
|
476
|
-
if (xAxis?.formatLabel) {
|
|
477
|
-
return xAxis.formatLabel(value, index);
|
|
478
|
-
}
|
|
479
|
-
if (isDateData(seriesData[0]?.data[0])) {
|
|
480
|
-
const date = formatDate(toDateUtil(value), xAxis?.dateFormat ?? DEFAULT_DATE_FORMAT);
|
|
481
|
-
return date.replace(",", ",\n");
|
|
482
|
-
}
|
|
483
|
-
return value;
|
|
484
|
-
},
|
|
485
|
-
fontSize: 10,
|
|
486
|
-
fontFamily,
|
|
487
|
-
rotate: xAxis?.rotateLabels ?? DEFAULT_ROTATE_LABELS,
|
|
488
|
-
overflow: "truncate",
|
|
489
|
-
},
|
|
490
|
-
data: seriesData[0]?.data.map(data => (isDateData(data) ? data.date : data.key)),
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
dataZoom: showDataZoom
|
|
494
|
-
? [
|
|
495
|
-
{
|
|
496
|
-
type: "slider",
|
|
497
|
-
id: "insideX",
|
|
498
|
-
xAxisIndex: 0,
|
|
499
|
-
startValue: 0,
|
|
500
|
-
endValue: (seriesData[0]?.data.length ?? 0) > 10 ? 10 : (seriesData[0]?.data.length ?? 0),
|
|
501
|
-
showDetail: false,
|
|
502
|
-
},
|
|
503
|
-
]
|
|
504
|
-
: [],
|
|
505
|
-
yAxis: [
|
|
506
|
-
{
|
|
507
|
-
type: "value",
|
|
508
|
-
axisLabel: {
|
|
509
|
-
fontSize: 10,
|
|
510
|
-
fontFamily,
|
|
511
|
-
formatter: yAxisLabelFormatter,
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
],
|
|
515
|
-
series: seriesData,
|
|
516
|
-
}, showLoading: loading, style: { height: "100%", minHeight: "150px", width: "100%" } }) })] }));
|
|
517
|
-
};
|
|
518
|
-
const cvaChartRoot$1 = cvaMerge(["flex", "flex-col", "items-center", "h-full"]);
|
|
519
|
-
|
|
520
285
|
/**
|
|
521
286
|
* Limits the data set to the given limit.
|
|
522
287
|
* If the data set is larger than the limit, the data set is limited to the limit and the rest of the data is added to the "Others" group.
|
|
@@ -722,9 +487,9 @@ const DonutChart = ({ data, size = "full", loading = false, onClick, className,
|
|
|
722
487
|
if (loading) {
|
|
723
488
|
return jsx(Spinner, { centering: "centered", "data-testid": dataTestId ? `${dataTestId}-loading` : "donut-chart-loading" });
|
|
724
489
|
}
|
|
725
|
-
return (jsxs("div", { className: cvaChartRoot({ className }), "data-testid": dataTestId, ref: mergedRef, style: style, children: [jsx("div", { className: cvaChartContainer(), children: jsx(Chart, { className: cvaChart({ size }), "data-testid": dataTestId ? `chart-${dataTestId}` : undefined, onChartReady: handleChartReady, onClick: handleChartClick, onEvents: handleChartEvents, options: chartOptions, style: { width: "100%", height: "100%" } }) }), size === "full" && (jsx("div", { className: cvaLegend(), "data-testid": "legend", children: limitedData.map((item, index) => (jsx(LegendItem, { className: "p-1.5 py-0.5", color: item.color ?? chartColor(index), count: item.value, "data-testid": `legend-${item.id}`, disabled: (item.value ?? 0) === 0, hideValue: hideLegendValues, label: item.name, onClick: onClick ? () => onClick(item) : undefined, onMouseEnter: () => handleLegendMouseEnter(item), onMouseLeave: handleLegendMouseLeave, selected: item.selected, unit: unit }, item.id))) }))] }));
|
|
490
|
+
return (jsxs("div", { className: cvaChartRoot$1({ className }), "data-testid": dataTestId, ref: mergedRef, style: style, children: [jsx("div", { className: cvaChartContainer(), children: jsx(Chart, { className: cvaChart({ size }), "data-testid": dataTestId ? `chart-${dataTestId}` : undefined, onChartReady: handleChartReady, onClick: handleChartClick, onEvents: handleChartEvents, options: chartOptions, style: { width: "100%", height: "100%" } }) }), size === "full" && (jsx("div", { className: cvaLegend(), "data-testid": "legend", children: limitedData.map((item, index) => (jsx(LegendItem, { className: "p-1.5 py-0.5", color: item.color ?? chartColor(index), count: item.value, "data-testid": `legend-${item.id}`, disabled: (item.value ?? 0) === 0, hideValue: hideLegendValues, label: item.name, onClick: onClick ? () => onClick(item) : undefined, onMouseEnter: () => handleLegendMouseEnter(item), onMouseLeave: handleLegendMouseLeave, selected: item.selected, unit: unit }, item.id))) }))] }));
|
|
726
491
|
};
|
|
727
|
-
const cvaChartRoot = cvaMerge([
|
|
492
|
+
const cvaChartRoot$1 = cvaMerge([
|
|
728
493
|
"flex",
|
|
729
494
|
"w-full",
|
|
730
495
|
"h-full",
|
|
@@ -747,4 +512,134 @@ const cvaChart = cvaMerge(["flex-0", "max-w-[200px]", "max-h-[200px]", "place-se
|
|
|
747
512
|
});
|
|
748
513
|
const cvaLegend = cvaMerge(["flex", "overflow-auto", "justify-start", "flex-col", "flex-1"]);
|
|
749
514
|
|
|
750
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
517
|
+
* with sensible defaults for displaying categorical or time-series data.
|
|
518
|
+
*
|
|
519
|
+
* ### When to use
|
|
520
|
+
* - To compare values across categories
|
|
521
|
+
* - To show trends over time with discrete intervals
|
|
522
|
+
* - When you need multiple series displayed side by side
|
|
523
|
+
*
|
|
524
|
+
* @example Bar chart with date-based data
|
|
525
|
+
* ```tsx
|
|
526
|
+
* import { BarChart } from "@trackunit/react-chart-components";
|
|
527
|
+
*
|
|
528
|
+
* const UtilizationChart = () => (
|
|
529
|
+
* <BarChart
|
|
530
|
+
* series={{
|
|
531
|
+
* name: "Utilization",
|
|
532
|
+
* data: [
|
|
533
|
+
* { date: "2024-01-01", value: 85 },
|
|
534
|
+
* { date: "2024-01-02", value: 72 },
|
|
535
|
+
* { date: "2024-01-03", value: 91 },
|
|
536
|
+
* ],
|
|
537
|
+
* }}
|
|
538
|
+
* units="%"
|
|
539
|
+
* onClick={(event) => console.log("Clicked bar:", event.data)}
|
|
540
|
+
* />
|
|
541
|
+
* );
|
|
542
|
+
* ```
|
|
543
|
+
* @example Multi-series bar chart with data zoom
|
|
544
|
+
* ```tsx
|
|
545
|
+
* import { BarChart } from "@trackunit/react-chart-components";
|
|
546
|
+
*
|
|
547
|
+
* const ComparisonChart = () => (
|
|
548
|
+
* <BarChart
|
|
549
|
+
* series={[
|
|
550
|
+
* { name: "2023", color: "#3b82f6", data: [{ key: "Q1", value: 100 }, { key: "Q2", value: 120 }] },
|
|
551
|
+
* { name: "2024", color: "#10b981", data: [{ key: "Q1", value: 130 }, { key: "Q2", value: 145 }] },
|
|
552
|
+
* ]}
|
|
553
|
+
* units="units"
|
|
554
|
+
* showDataZoom={true}
|
|
555
|
+
* />
|
|
556
|
+
* );
|
|
557
|
+
* ```
|
|
558
|
+
* @param {BarChartProps} props - The props for the Chart component
|
|
559
|
+
* @returns {ReactElement} Chart component
|
|
560
|
+
*/
|
|
561
|
+
const BarChart = ({ series, loading = false, onClick, className, style, "data-testid": dataTestId, units, showDataZoom = false, ref, }) => {
|
|
562
|
+
const { formatDate } = useDateAndTime();
|
|
563
|
+
const { chartColor } = useChartColor();
|
|
564
|
+
const seriesData = useMemo(() => {
|
|
565
|
+
const seriesAsArray = Array.isArray(series) ? series : series !== undefined ? [series] : [];
|
|
566
|
+
return seriesAsArray.map((s, i) => ({
|
|
567
|
+
...s,
|
|
568
|
+
color: s.color || chartColor(i),
|
|
569
|
+
type: "bar",
|
|
570
|
+
cursor: "arrow",
|
|
571
|
+
barGap: "0",
|
|
572
|
+
data: s.data.map(d => ({
|
|
573
|
+
...d,
|
|
574
|
+
})),
|
|
575
|
+
}));
|
|
576
|
+
}, [series, chartColor]);
|
|
577
|
+
const isDateData = (data) => !!data && "date" in data;
|
|
578
|
+
return (jsxs("div", { className: cvaChartRoot({ className }), "data-testid": dataTestId, ref: ref, style: style, children: [jsx("div", { className: "flex-0 flex flex-row gap-2 place-self-end pr-8", "data-testid": "legend", children: seriesData.map((item, index) => (jsx(LegendItem, { color: item.color || chartColor(index), "data-testid": `legend-${item.name}`, label: item.name }, item.name))) }), jsx("div", { className: "flex w-full flex-1", children: jsx(Chart, { "data-testid": "bar-chart", onClick: onClick
|
|
579
|
+
? (e) => {
|
|
580
|
+
onClick(e);
|
|
581
|
+
}
|
|
582
|
+
: undefined, options: {
|
|
583
|
+
tooltip: {
|
|
584
|
+
trigger: "item",
|
|
585
|
+
confine: true,
|
|
586
|
+
valueFormatter: value => `${value} ${units ?? ""}`,
|
|
587
|
+
},
|
|
588
|
+
grid: {
|
|
589
|
+
left: "2%",
|
|
590
|
+
right: "2%",
|
|
591
|
+
bottom: "10%",
|
|
592
|
+
top: "10%",
|
|
593
|
+
containLabel: true,
|
|
594
|
+
},
|
|
595
|
+
xAxis: [
|
|
596
|
+
{
|
|
597
|
+
type: "category",
|
|
598
|
+
axisLabel: {
|
|
599
|
+
formatter: value => {
|
|
600
|
+
if (isDateData(seriesData[0]?.data[0])) {
|
|
601
|
+
const date = formatDate(toDateUtil(value), {
|
|
602
|
+
selectFormat: "dateOnly",
|
|
603
|
+
dateFormat: "medium",
|
|
604
|
+
});
|
|
605
|
+
return date.replace(",", ",\n");
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
},
|
|
609
|
+
fontSize: 10,
|
|
610
|
+
fontFamily,
|
|
611
|
+
rotate: 45,
|
|
612
|
+
overflow: "truncate",
|
|
613
|
+
},
|
|
614
|
+
data: seriesData[0]?.data
|
|
615
|
+
.filter(({ value }) => Boolean(value))
|
|
616
|
+
.map(data => (isDateData(data) ? data.date : data.key)),
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
dataZoom: showDataZoom
|
|
620
|
+
? [
|
|
621
|
+
{
|
|
622
|
+
type: "slider",
|
|
623
|
+
id: "insideX",
|
|
624
|
+
xAxisIndex: 0,
|
|
625
|
+
startValue: 0,
|
|
626
|
+
endValue: (seriesData[0]?.data.length ?? 0) > 10 ? 10 : (seriesData[0]?.data.length ?? 0),
|
|
627
|
+
showDetail: false,
|
|
628
|
+
},
|
|
629
|
+
]
|
|
630
|
+
: [],
|
|
631
|
+
yAxis: [
|
|
632
|
+
{
|
|
633
|
+
type: "value",
|
|
634
|
+
axisLabel: {
|
|
635
|
+
fontSize: 10,
|
|
636
|
+
fontFamily,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
series: seriesData,
|
|
641
|
+
}, showLoading: loading, style: { height: "100%", minHeight: "150px", width: "100%" } }) })] }));
|
|
642
|
+
};
|
|
643
|
+
const cvaChartRoot = cvaMerge(["flex", "flex-col", "items-center", "h-full"]);
|
|
644
|
+
|
|
645
|
+
export { BarChart, Chart, DonutChart, EChart, LegendItem, useChartColor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-chart-components",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.42-alpha-7bce9c1cf2a.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"migrations": "./migrations.json",
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"echarts": "5.6.0",
|
|
12
|
-
"@trackunit/date-and-time-utils": "1.13.
|
|
13
|
-
"@trackunit/react-date-and-time-hooks": "2.1.
|
|
14
|
-
"@trackunit/ui-design-tokens": "1.13.
|
|
15
|
-
"@trackunit/shared-utils": "1.15.
|
|
16
|
-
"@trackunit/css-class-variance-utilities": "1.13.
|
|
17
|
-
"@trackunit/react-components": "2.1.
|
|
12
|
+
"@trackunit/date-and-time-utils": "1.13.46-alpha-7bce9c1cf2a.0",
|
|
13
|
+
"@trackunit/react-date-and-time-hooks": "2.1.41-alpha-7bce9c1cf2a.0",
|
|
14
|
+
"@trackunit/ui-design-tokens": "1.13.43-alpha-7bce9c1cf2a.0",
|
|
15
|
+
"@trackunit/shared-utils": "1.15.43-alpha-7bce9c1cf2a.0",
|
|
16
|
+
"@trackunit/css-class-variance-utilities": "1.13.43-alpha-7bce9c1cf2a.0",
|
|
17
|
+
"@trackunit/react-components": "2.1.39-alpha-7bce9c1cf2a.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@apollo/client": "^3.13.8",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type TemporalFormat } from "@trackunit/date-and-time-utils";
|
|
2
1
|
import { CommonProps, Refable, type Styleable } from "@trackunit/react-components";
|
|
3
2
|
import { ECElementEvent } from "echarts";
|
|
4
3
|
import { ReactElement } from "react";
|
|
@@ -11,10 +10,6 @@ export interface BarChartData<TProps extends object = object> {
|
|
|
11
10
|
* If selected, it'll be highlighted
|
|
12
11
|
*/
|
|
13
12
|
selected?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
* Per-bar color. Falls back to the series color / auto color when omitted.
|
|
16
|
-
*/
|
|
17
|
-
color?: string;
|
|
18
13
|
/**
|
|
19
14
|
* Supply the original object that this chart data item was constructed from. It'll be available on callbacks
|
|
20
15
|
*/
|
|
@@ -37,88 +32,6 @@ export interface SeriesData<TProps extends object> {
|
|
|
37
32
|
color?: string;
|
|
38
33
|
data: Array<BarChartGenericData<TProps>> | Array<BarChartDateData<TProps>>;
|
|
39
34
|
}
|
|
40
|
-
/**
|
|
41
|
-
* A single entry in a tooltip payload. For `trigger: "item"` there is one entry;
|
|
42
|
-
* for `trigger: "axis"` there is one entry per series at the hovered category.
|
|
43
|
-
*/
|
|
44
|
-
export interface BarChartTooltipEntry<TProps extends object = object> {
|
|
45
|
-
/**
|
|
46
|
-
* The name of the series this entry belongs to
|
|
47
|
-
*/
|
|
48
|
-
seriesName: string;
|
|
49
|
-
/**
|
|
50
|
-
* The numeric value of the hovered bar
|
|
51
|
-
*/
|
|
52
|
-
value: number | undefined;
|
|
53
|
-
/**
|
|
54
|
-
* The resolved color of the bar
|
|
55
|
-
*/
|
|
56
|
-
color: string;
|
|
57
|
-
/**
|
|
58
|
-
* The colored-dot marker HTML that ECharts provides
|
|
59
|
-
*/
|
|
60
|
-
marker: string;
|
|
61
|
-
/**
|
|
62
|
-
* The index of the data item within its series
|
|
63
|
-
*/
|
|
64
|
-
dataIndex: number;
|
|
65
|
-
/**
|
|
66
|
-
* The original object the data item was constructed from
|
|
67
|
-
*/
|
|
68
|
-
original?: TProps;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Typed tooltip payload passed to `tooltip.formatter`.
|
|
72
|
-
*/
|
|
73
|
-
export interface BarChartTooltipParams<TProps extends object = object> {
|
|
74
|
-
/**
|
|
75
|
-
* The x-axis category/date value being hovered
|
|
76
|
-
*/
|
|
77
|
-
axisValue: string;
|
|
78
|
-
/**
|
|
79
|
-
* One entry for `trigger: "item"`, one entry per series for `trigger: "axis"`
|
|
80
|
-
*/
|
|
81
|
-
entries: Array<BarChartTooltipEntry<TProps>>;
|
|
82
|
-
}
|
|
83
|
-
export interface BarChartXAxisOptions {
|
|
84
|
-
/**
|
|
85
|
-
* Date granularity for date-based x-axis labels. Defaults to
|
|
86
|
-
* `{ selectFormat: "dateOnly", dateFormat: "medium" }`.
|
|
87
|
-
*/
|
|
88
|
-
dateFormat?: TemporalFormat;
|
|
89
|
-
/**
|
|
90
|
-
* Full control over the x-axis label string. Wins over `dateFormat`.
|
|
91
|
-
*/
|
|
92
|
-
formatLabel?: (value: string, index: number) => string;
|
|
93
|
-
/**
|
|
94
|
-
* Rotation of the x-axis labels in degrees. Defaults to `45`; `0` renders horizontal labels.
|
|
95
|
-
*/
|
|
96
|
-
rotateLabels?: number;
|
|
97
|
-
}
|
|
98
|
-
export interface BarChartYAxisOptions {
|
|
99
|
-
/**
|
|
100
|
-
* Locale-aware compact y-axis labels (e.g. `40,000 → 40K`). Defaults to `false`.
|
|
101
|
-
*/
|
|
102
|
-
abbreviate?: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Full control over the y-axis label string. Wins over `abbreviate`.
|
|
105
|
-
*/
|
|
106
|
-
formatLabel?: (value: number) => string;
|
|
107
|
-
}
|
|
108
|
-
export interface BarChartTooltipOptions<TProps extends object = object> {
|
|
109
|
-
/**
|
|
110
|
-
* Format a single tooltip value. Defaults to `` `${value} ${units}` ``.
|
|
111
|
-
*/
|
|
112
|
-
formatValue?: (value: number) => string;
|
|
113
|
-
/**
|
|
114
|
-
* Build the entire tooltip from a typed payload. Wins over `formatValue`.
|
|
115
|
-
*/
|
|
116
|
-
formatter?: (params: BarChartTooltipParams<TProps>) => string;
|
|
117
|
-
/**
|
|
118
|
-
* Tooltip trigger mode. Defaults to `"item"`.
|
|
119
|
-
*/
|
|
120
|
-
trigger?: "item" | "axis";
|
|
121
|
-
}
|
|
122
35
|
export interface BarChartProps<TProps extends object> extends CommonProps, Refable<HTMLDivElement>, Styleable {
|
|
123
36
|
/**
|
|
124
37
|
* Array of series of data points to show
|
|
@@ -140,30 +53,11 @@ export interface BarChartProps<TProps extends object> extends CommonProps, Refab
|
|
|
140
53
|
* Show data zoom
|
|
141
54
|
*/
|
|
142
55
|
showDataZoom?: boolean;
|
|
143
|
-
/**
|
|
144
|
-
* Curated x-axis customization (label formatting and rotation).
|
|
145
|
-
*/
|
|
146
|
-
xAxis?: BarChartXAxisOptions;
|
|
147
|
-
/**
|
|
148
|
-
* Curated y-axis customization (label formatting and abbreviation).
|
|
149
|
-
*/
|
|
150
|
-
yAxis?: BarChartYAxisOptions;
|
|
151
|
-
/**
|
|
152
|
-
* Curated tooltip customization (value/payload formatting and trigger mode).
|
|
153
|
-
*/
|
|
154
|
-
tooltip?: BarChartTooltipOptions<TProps>;
|
|
155
|
-
/**
|
|
156
|
-
* Toggle the custom legend row. Defaults to `true`.
|
|
157
|
-
*/
|
|
158
|
-
showLegend?: boolean;
|
|
159
56
|
}
|
|
160
57
|
/**
|
|
161
58
|
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
162
59
|
* with sensible defaults for displaying categorical or time-series data.
|
|
163
60
|
*
|
|
164
|
-
* All customization props are optional and default to the component's original rendering, so
|
|
165
|
-
* existing usages are unaffected.
|
|
166
|
-
*
|
|
167
61
|
* ### When to use
|
|
168
62
|
* - To compare values across categories
|
|
169
63
|
* - To show trends over time with discrete intervals
|
|
@@ -203,36 +97,7 @@ export interface BarChartProps<TProps extends object> extends CommonProps, Refab
|
|
|
203
97
|
* />
|
|
204
98
|
* );
|
|
205
99
|
* ```
|
|
206
|
-
* @example Abbreviated y-axis, horizontal x-axis labels and a hidden legend
|
|
207
|
-
* ```tsx
|
|
208
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
209
|
-
*
|
|
210
|
-
* const EmissionsChart = () => (
|
|
211
|
-
* <BarChart
|
|
212
|
-
* series={{ name: "Emissions", data: [{ key: "Excavator", value: 40000, color: "#3b82f6" }] }}
|
|
213
|
-
* units="t"
|
|
214
|
-
* yAxis={{ abbreviate: true }}
|
|
215
|
-
* xAxis={{ rotateLabels: 0, formatLabel: (value) => value.slice(0, 10) }}
|
|
216
|
-
* showLegend={false}
|
|
217
|
-
* />
|
|
218
|
-
* );
|
|
219
|
-
* ```
|
|
220
|
-
* @example Custom tooltip from the typed payload
|
|
221
|
-
* ```tsx
|
|
222
|
-
* import { BarChart } from "@trackunit/react-chart-components";
|
|
223
|
-
*
|
|
224
|
-
* const TrendChart = () => (
|
|
225
|
-
* <BarChart
|
|
226
|
-
* series={{ name: "Emissions", data: [{ date: "2024-01-01", value: 120 }] }}
|
|
227
|
-
* tooltip={{
|
|
228
|
-
* trigger: "axis",
|
|
229
|
-
* formatter: ({ axisValue, entries }) =>
|
|
230
|
-
* `${axisValue}<br/>${entries.map((e) => `${e.marker} ${e.seriesName}: ${e.value ?? 0}`).join("<br/>")}`,
|
|
231
|
-
* }}
|
|
232
|
-
* />
|
|
233
|
-
* );
|
|
234
|
-
* ```
|
|
235
100
|
* @param {BarChartProps} props - The props for the Chart component
|
|
236
101
|
* @returns {ReactElement} Chart component
|
|
237
102
|
*/
|
|
238
|
-
export declare const BarChart: <TProps extends object>({ series, loading, onClick, className, style, "data-testid": dataTestId, units, showDataZoom,
|
|
103
|
+
export declare const BarChart: <TProps extends object>({ series, loading, onClick, className, style, "data-testid": dataTestId, units, showDataZoom, ref, }: BarChartProps<TProps>) => ReactElement;
|
package/src/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export * from "./BarChart/BarChart";
|
|
2
1
|
export * from "./Chart/Chart";
|
|
3
2
|
export * from "./DonutChart/DonutChart";
|
|
4
|
-
export * from "./
|
|
3
|
+
export * from "./BarChart/BarChart";
|
|
5
4
|
export * from "./LegendItem/LegendItem";
|
|
6
5
|
export * from "./utils/useChartColor";
|
|
7
|
-
export * from "./
|
|
6
|
+
export * from "./EChart/EChart";
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Locale-aware number formatting helpers for charts. Mirrors `useChartColor`:
|
|
3
|
-
* a hook that returns memoized formatting functions so chart consumers share one
|
|
4
|
-
* consistent compact-number style instead of hand-rolling `>= 1000 → k` logic.
|
|
5
|
-
*
|
|
6
|
-
* @returns {{ formatCompact: (value: number) => string }} An object exposing
|
|
7
|
-
* `formatCompact`, a locale-aware compact formatter (e.g. `40000 → "40K"`, `40_000_000 → "40M"`).
|
|
8
|
-
* @example Abbreviate a y-axis value
|
|
9
|
-
* ```tsx
|
|
10
|
-
* const { formatCompact } = useChartNumberFormat();
|
|
11
|
-
* formatCompact(40000); // "40K"
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
export declare const useChartNumberFormat: () => {
|
|
15
|
-
formatCompact: (value: number) => string;
|
|
16
|
-
};
|