@trackunit/react-chart-components 2.1.43-alpha-6a85feaada3.0 → 2.1.43
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 +243 -137
- package/index.esm.js +243 -138
- package/package.json +7 -7
- package/src/BarChart/BarChart.d.ts +136 -1
- package/src/index.d.ts +3 -2
- package/src/utils/useChartNumberFormat.d.ts +16 -0
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 reactComponents = require('@trackunit/react-components');
|
|
5
4
|
var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
|
|
6
|
-
var sharedUtils = require('@trackunit/shared-utils');
|
|
7
|
-
var echarts = require('echarts');
|
|
8
|
-
var react = require('react');
|
|
9
|
-
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
10
5
|
var dateAndTimeUtils = require('@trackunit/date-and-time-utils');
|
|
11
6
|
var reactDateAndTimeHooks = require('@trackunit/react-date-and-time-hooks');
|
|
7
|
+
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
8
|
+
var react = require('react');
|
|
9
|
+
var reactComponents = require('@trackunit/react-components');
|
|
10
|
+
var sharedUtils = require('@trackunit/shared-utils');
|
|
11
|
+
var echarts = require('echarts');
|
|
12
12
|
|
|
13
13
|
function _interopNamespaceDefault(e) {
|
|
14
14
|
var n = Object.create(null);
|
|
@@ -303,6 +303,241 @@ 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
|
+
|
|
306
541
|
/**
|
|
307
542
|
* Limits the data set to the given limit.
|
|
308
543
|
* 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.
|
|
@@ -508,9 +743,9 @@ const DonutChart = ({ data, size = "full", loading = false, onClick, className,
|
|
|
508
743
|
if (loading) {
|
|
509
744
|
return jsxRuntime.jsx(reactComponents.Spinner, { centering: "centered", "data-testid": dataTestId ? `${dataTestId}-loading` : "donut-chart-loading" });
|
|
510
745
|
}
|
|
511
|
-
return (jsxRuntime.jsxs("div", { className: cvaChartRoot
|
|
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))) }))] }));
|
|
512
747
|
};
|
|
513
|
-
const cvaChartRoot
|
|
748
|
+
const cvaChartRoot = cssClassVarianceUtilities.cvaMerge([
|
|
514
749
|
"flex",
|
|
515
750
|
"w-full",
|
|
516
751
|
"h-full",
|
|
@@ -533,139 +768,10 @@ const cvaChart = cssClassVarianceUtilities.cvaMerge(["flex-0", "max-w-[200px]",
|
|
|
533
768
|
});
|
|
534
769
|
const cvaLegend = cssClassVarianceUtilities.cvaMerge(["flex", "overflow-auto", "justify-start", "flex-col", "flex-1"]);
|
|
535
770
|
|
|
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
|
-
|
|
666
771
|
exports.BarChart = BarChart;
|
|
667
772
|
exports.Chart = Chart;
|
|
668
773
|
exports.DonutChart = DonutChart;
|
|
669
774
|
exports.EChart = EChart;
|
|
670
775
|
exports.LegendItem = LegendItem;
|
|
671
776
|
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 { Spinner, Text, useMergeRefs } from '@trackunit/react-components';
|
|
3
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
|
+
import { Spinner, Text, useMergeRefs } from '@trackunit/react-components';
|
|
4
8
|
import { objectKeys } from '@trackunit/shared-utils';
|
|
5
9
|
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,6 +282,241 @@ 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
|
+
|
|
285
520
|
/**
|
|
286
521
|
* Limits the data set to the given limit.
|
|
287
522
|
* 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.
|
|
@@ -487,9 +722,9 @@ const DonutChart = ({ data, size = "full", loading = false, onClick, className,
|
|
|
487
722
|
if (loading) {
|
|
488
723
|
return jsx(Spinner, { centering: "centered", "data-testid": dataTestId ? `${dataTestId}-loading` : "donut-chart-loading" });
|
|
489
724
|
}
|
|
490
|
-
return (jsxs("div", { className: cvaChartRoot
|
|
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))) }))] }));
|
|
491
726
|
};
|
|
492
|
-
const cvaChartRoot
|
|
727
|
+
const cvaChartRoot = cvaMerge([
|
|
493
728
|
"flex",
|
|
494
729
|
"w-full",
|
|
495
730
|
"h-full",
|
|
@@ -512,134 +747,4 @@ const cvaChart = cvaMerge(["flex-0", "max-w-[200px]", "max-h-[200px]", "place-se
|
|
|
512
747
|
});
|
|
513
748
|
const cvaLegend = cvaMerge(["flex", "overflow-auto", "justify-start", "flex-col", "flex-1"]);
|
|
514
749
|
|
|
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 };
|
|
750
|
+
export { BarChart, Chart, DonutChart, EChart, LegendItem, useChartColor, useChartNumberFormat };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-chart-components",
|
|
3
|
-
"version": "2.1.43
|
|
3
|
+
"version": "2.1.43",
|
|
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.42
|
|
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.40
|
|
12
|
+
"@trackunit/date-and-time-utils": "1.13.46",
|
|
13
|
+
"@trackunit/react-date-and-time-hooks": "2.1.42",
|
|
14
|
+
"@trackunit/ui-design-tokens": "1.13.43",
|
|
15
|
+
"@trackunit/shared-utils": "1.15.43",
|
|
16
|
+
"@trackunit/css-class-variance-utilities": "1.13.43",
|
|
17
|
+
"@trackunit/react-components": "2.1.40"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@apollo/client": "^3.13.8",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TemporalFormat } from "@trackunit/date-and-time-utils";
|
|
1
2
|
import { CommonProps, Refable, type Styleable } from "@trackunit/react-components";
|
|
2
3
|
import { ECElementEvent } from "echarts";
|
|
3
4
|
import { ReactElement } from "react";
|
|
@@ -10,6 +11,10 @@ export interface BarChartData<TProps extends object = object> {
|
|
|
10
11
|
* If selected, it'll be highlighted
|
|
11
12
|
*/
|
|
12
13
|
selected?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Per-bar color. Falls back to the series color / auto color when omitted.
|
|
16
|
+
*/
|
|
17
|
+
color?: string;
|
|
13
18
|
/**
|
|
14
19
|
* Supply the original object that this chart data item was constructed from. It'll be available on callbacks
|
|
15
20
|
*/
|
|
@@ -32,6 +37,88 @@ export interface SeriesData<TProps extends object> {
|
|
|
32
37
|
color?: string;
|
|
33
38
|
data: Array<BarChartGenericData<TProps>> | Array<BarChartDateData<TProps>>;
|
|
34
39
|
}
|
|
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
|
+
}
|
|
35
122
|
export interface BarChartProps<TProps extends object> extends CommonProps, Refable<HTMLDivElement>, Styleable {
|
|
36
123
|
/**
|
|
37
124
|
* Array of series of data points to show
|
|
@@ -53,11 +140,30 @@ export interface BarChartProps<TProps extends object> extends CommonProps, Refab
|
|
|
53
140
|
* Show data zoom
|
|
54
141
|
*/
|
|
55
142
|
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;
|
|
56
159
|
}
|
|
57
160
|
/**
|
|
58
161
|
* Create a BarChart with automatic legends and formatting. Built on top of the Chart component
|
|
59
162
|
* with sensible defaults for displaying categorical or time-series data.
|
|
60
163
|
*
|
|
164
|
+
* All customization props are optional and default to the component's original rendering, so
|
|
165
|
+
* existing usages are unaffected.
|
|
166
|
+
*
|
|
61
167
|
* ### When to use
|
|
62
168
|
* - To compare values across categories
|
|
63
169
|
* - To show trends over time with discrete intervals
|
|
@@ -97,7 +203,36 @@ export interface BarChartProps<TProps extends object> extends CommonProps, Refab
|
|
|
97
203
|
* />
|
|
98
204
|
* );
|
|
99
205
|
* ```
|
|
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
|
+
* ```
|
|
100
235
|
* @param {BarChartProps} props - The props for the Chart component
|
|
101
236
|
* @returns {ReactElement} Chart component
|
|
102
237
|
*/
|
|
103
|
-
export declare const BarChart: <TProps extends object>({ series, loading, onClick, className, style, "data-testid": dataTestId, units, showDataZoom, ref, }: BarChartProps<TProps>) => ReactElement;
|
|
238
|
+
export declare const BarChart: <TProps extends object>({ series, loading, onClick, className, style, "data-testid": dataTestId, units, showDataZoom, xAxis, yAxis, tooltip, showLegend, ref, }: BarChartProps<TProps>) => ReactElement;
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export * from "./BarChart/BarChart";
|
|
1
2
|
export * from "./Chart/Chart";
|
|
2
3
|
export * from "./DonutChart/DonutChart";
|
|
3
|
-
export * from "./
|
|
4
|
+
export * from "./EChart/EChart";
|
|
4
5
|
export * from "./LegendItem/LegendItem";
|
|
5
6
|
export * from "./utils/useChartColor";
|
|
6
|
-
export * from "./
|
|
7
|
+
export * from "./utils/useChartNumberFormat";
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
};
|