@scality/core-ui 0.176.0 → 0.177.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/dist/components/barchartv2/Barchart.component.d.ts +1 -1
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +10 -6
- package/dist/components/barchartv2/BarchartTooltip.d.ts +3 -2
- package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -1
- package/dist/components/barchartv2/BarchartTooltip.js +6 -8
- package/dist/components/barchartv2/utils.d.ts +6 -1
- package/dist/components/barchartv2/utils.d.ts.map +1 -1
- package/dist/components/barchartv2/utils.js +34 -8
- package/dist/components/charttooltip/ChartTooltip.d.ts +23 -0
- package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -1
- package/dist/components/charttooltip/ChartTooltip.js +83 -1
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -1
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +27 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +1 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +19 -59
- package/dist/components/globalhealthbar/components/HealthBarXAxis.js +1 -1
- package/dist/components/globalhealthbar/useHealthBarData.d.ts +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -1
- package/dist/components/globalhealthbar/useHealthBarData.js +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.spec.js +2 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +43 -47
- package/dist/components/linetimeseriechart/utils.js +2 -2
- package/dist/style/theme.js +1 -1
- package/package.json +1 -1
- package/src/lib/components/barchartv2/Barchart.component.tsx +19 -12
- package/src/lib/components/barchartv2/BarchartTooltip.test.tsx +30 -0
- package/src/lib/components/barchartv2/BarchartTooltip.tsx +21 -8
- package/src/lib/components/barchartv2/utils.test.ts +72 -17
- package/src/lib/components/barchartv2/utils.ts +39 -7
- package/src/lib/components/charttooltip/ChartTooltip.tsx +134 -1
- package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +56 -11
- package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +75 -117
- package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +1 -1
- package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +2 -0
- package/src/lib/components/globalhealthbar/useHealthBarData.ts +2 -0
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +86 -82
- package/src/lib/components/linetimeseriechart/utils.test.ts +3 -3
- package/src/lib/components/linetimeseriechart/utils.ts +2 -2
- package/src/lib/style/theme.ts +1 -1
- package/stories/BarChart/barchart.stories.tsx +23 -8
|
@@ -3,6 +3,7 @@ export interface Alert {
|
|
|
3
3
|
startsAt: string;
|
|
4
4
|
endsAt: string;
|
|
5
5
|
severity: 'warning' | 'critical' | 'unavailable';
|
|
6
|
+
key: string;
|
|
6
7
|
}
|
|
7
8
|
export declare const useHealthBarData: (alerts: Alert[], startTimestamp: number, endTimestamp: number, id: string) => {
|
|
8
9
|
chartData: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHealthBarData.d.ts","sourceRoot":"","sources":["../../../src/lib/components/globalhealthbar/useHealthBarData.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"useHealthBarData.d.ts","sourceRoot":"","sources":["../../../src/lib/components/globalhealthbar/useHealthBarData.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;IACjD,GAAG,EAAE,MAAM,CAAC;CACb;AAED,eAAO,MAAM,gBAAgB,WACnB,KAAK,EAAE,kBACC,MAAM,gBACR,MAAM,MAChB,MAAM;;;;;;;;;;CA6DX,CAAC"}
|
|
@@ -20,6 +20,7 @@ export const useHealthBarData = (alerts, startTimestamp, endTimestamp, id) => {
|
|
|
20
20
|
// Store alert data separately for tooltip access
|
|
21
21
|
alertsMapData[uniqueKey] = {
|
|
22
22
|
...alert,
|
|
23
|
+
key: uniqueKey, // Add the consistent key to the alert object
|
|
23
24
|
};
|
|
24
25
|
});
|
|
25
26
|
// Chart data - ready for BarChart (as array)
|
|
@@ -10,6 +10,7 @@ describe('useHealthBarData', () => {
|
|
|
10
10
|
severity,
|
|
11
11
|
startsAt,
|
|
12
12
|
endsAt,
|
|
13
|
+
key: `${severity}_${startsAt}`,
|
|
13
14
|
});
|
|
14
15
|
describe('Alert Filtering', () => {
|
|
15
16
|
it('should include alerts that are completely within the time range', () => {
|
|
@@ -93,6 +94,7 @@ describe('useHealthBarData', () => {
|
|
|
93
94
|
severity: 'warning',
|
|
94
95
|
startsAt: '2023-12-01T02:00:00Z',
|
|
95
96
|
endsAt: '2023-12-01T04:00:00Z',
|
|
97
|
+
key: 'warning_0',
|
|
96
98
|
});
|
|
97
99
|
});
|
|
98
100
|
it('should handle multiple alerts with correct indexing', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linetimeseriechart.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,mBAAmB,EAGpB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAiD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"linetimeseriechart.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,mBAAmB,EAGpB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAiD,MAAM,OAAO,CAAC;AAiCtE,MAAM,MAAM,KAAK,GAAG;IAElB,QAAQ,EAAE,MAAM,CAAC;IAEjB,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAEzC,eAAe,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAEtE,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,SAAS,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IACrC,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;CAC7B,CAAC;AAGF,KAAK,qBAAqB,GAAG;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,MAAM,EACF;QACE,KAAK,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;QAC3B,KAAK,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;KAC5B,GACD,SAAS,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CACzB,wBAAwB,GACxB,qBAAqB,CACxB,GAAG;IACF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;IACJ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,CACd,YAAY,EAAE,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,EACjD,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,KACd,KAAK,CAAC,SAAS,CAAC;CACtB,CAAC;AAiHF,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,KAAK,EACL,MAAM,EACN,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAiB,EACjB,UAAwB,EACxB,SAAqB,EACrB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,aAAa,EACb,GAAG,IAAI,EACR,EAAE,cAAc,2CAqWhB"}
|
|
@@ -1,62 +1,58 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { CartesianGrid, Line, LineChart, ReferenceLine, Tooltip, XAxis, YAxis, } from 'recharts';
|
|
3
3
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import styled, { useTheme } from 'styled-components';
|
|
5
|
-
import {
|
|
5
|
+
import { Stack } from '../../spacing';
|
|
6
6
|
import { fontSize } from '../../style/theme';
|
|
7
|
-
import { Box } from '../box/Box';
|
|
8
7
|
import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
|
|
9
|
-
import { Icon } from '../icon/Icon.component';
|
|
10
8
|
import { addMissingDataPoint, getUnitLabel, } from '../linetemporalchart/ChartUtil';
|
|
11
9
|
import { Loader } from '../loader/Loader.component';
|
|
12
|
-
import { ChartTitleText
|
|
13
|
-
import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component';
|
|
10
|
+
import { ChartTitleText } from '../text/Text.component';
|
|
14
11
|
import { formatXAxisLabel } from './utils';
|
|
15
|
-
import {
|
|
12
|
+
import { ChartTooltipPortal, ChartTooltipItem, ChartTooltipHeader, ChartTooltipItemsContainer, ChartTooltipSeparator, TooltipHeader, } from '../charttooltip/ChartTooltip';
|
|
16
13
|
import { LegendShape } from '../chartlegend/ChartLegend';
|
|
17
14
|
import { StyledResponsiveContainer } from '../barchartv2/Barchart.component';
|
|
15
|
+
import { getRoundReferenceValue, getTicks } from '../barchartv2/utils';
|
|
16
|
+
import { IconHelp } from '../iconhelper/IconHelper';
|
|
17
|
+
const maxWidthTooltip = { maxWidth: '20rem' };
|
|
18
18
|
const LineTemporalChartWrapper = styled.div `
|
|
19
19
|
display: flex;
|
|
20
20
|
flex-direction: column;
|
|
21
21
|
justify-content: flex-start;
|
|
22
22
|
`;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
align-items: center;
|
|
26
|
-
`;
|
|
27
|
-
const LineTimeSerieChartTooltip = ({ unitLabel, duration, isChartActive, tooltipProps, renderTooltip, hoveredValue, isSymmetrical, }) => {
|
|
28
|
-
const { active, payload, label } = tooltipProps;
|
|
23
|
+
const LineTimeSerieChartTooltip = ({ unitLabel, duration, isChartActive, tooltipProps, renderTooltip, hoveredValue, isSymmetrical, chartContainerRef, }) => {
|
|
24
|
+
const { active, payload, label, coordinate } = tooltipProps;
|
|
29
25
|
if (!active || !payload || !payload.length || !label || !isChartActive)
|
|
30
26
|
return null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
27
|
+
const tooltipContent = renderTooltip ? (renderTooltip(tooltipProps, unitLabel, duration)) : (_jsxs(_Fragment, { children: [_jsx(ChartTooltipHeader, { children: _jsx(TooltipHeader, { duration: duration, value: label }) }), _jsx(ChartTooltipItemsContainer, { children: (() => {
|
|
28
|
+
// We can't use the default itemSorter method because it's a custom tooltip.
|
|
29
|
+
// Sort the payload here instead
|
|
30
|
+
const sortedPayload = [...payload].sort((a, b) => {
|
|
31
|
+
const aValue = a.value;
|
|
32
|
+
const bValue = b.value;
|
|
33
|
+
if (aValue >= 0 && bValue >= 0) {
|
|
34
|
+
return bValue - aValue; // Higher positive values first
|
|
35
|
+
}
|
|
36
|
+
if (aValue < 0 && bValue < 0) {
|
|
37
|
+
return bValue - aValue; // Lower negative values first
|
|
38
|
+
}
|
|
39
|
+
return bValue - aValue; // Positives before negatives
|
|
40
|
+
});
|
|
41
|
+
// Find the transition point between positive and negative values
|
|
42
|
+
const separatorIndex = sortedPayload.findIndex((entry) => entry.value < 0);
|
|
43
|
+
const hasBothPositiveAndNegative = separatorIndex > 0 && separatorIndex < sortedPayload.length;
|
|
44
|
+
return sortedPayload.map((entry, index) => {
|
|
45
|
+
const legendIcon = (_jsx(LegendShape, { color: entry.color, shape: "line", chartColors: { [entry.color]: entry.color } }));
|
|
46
|
+
const isHovered = entry.name === hoveredValue;
|
|
47
|
+
const formattedValue = !Number.isFinite(entry.value)
|
|
48
|
+
? '-'
|
|
49
|
+
: `${entry.value.toFixed(2)} ${unitLabel}`;
|
|
50
|
+
return (_jsxs(React.Fragment, { children: [isSymmetrical &&
|
|
51
|
+
hasBothPositiveAndNegative &&
|
|
52
|
+
index === separatorIndex && _jsx(ChartTooltipSeparator, {}), _jsx(ChartTooltipItem, { label: entry.name, value: formattedValue, legendIcon: legendIcon, isHovered: isHovered })] }, index));
|
|
53
|
+
});
|
|
54
|
+
})() })] }));
|
|
55
|
+
return (_jsx(ChartTooltipPortal, { coordinate: coordinate, chartContainerRef: chartContainerRef, isVisible: active && isChartActive, children: tooltipContent }));
|
|
60
56
|
};
|
|
61
57
|
const isSymmetricalSeries = (series) => {
|
|
62
58
|
return 'above' in series && 'below' in series;
|
|
@@ -172,7 +168,8 @@ export function LineTimeSerieChart({ series, title, height, startingTimeStamp, i
|
|
|
172
168
|
const bottom = Math.abs(Math.min(...values));
|
|
173
169
|
const maxValue = Math.max(top, bottom);
|
|
174
170
|
const { valueBase, unitLabel } = getUnitLabel(unitRange !== null && unitRange !== void 0 ? unitRange : [], maxValue);
|
|
175
|
-
|
|
171
|
+
// Use round reference value to add extra padding to the top value
|
|
172
|
+
const topValue = getRoundReferenceValue(maxValue / valueBase);
|
|
176
173
|
const rechartsData = chartData.map((dataPoint) => {
|
|
177
174
|
const normalizedDataPoint = { ...dataPoint };
|
|
178
175
|
Object.entries(dataPoint).forEach(([key, value]) => {
|
|
@@ -222,15 +219,14 @@ export function LineTimeSerieChart({ series, title, height, startingTimeStamp, i
|
|
|
222
219
|
}, [series, getColor, selectedResources]);
|
|
223
220
|
// Format time for display the tick in the x axis
|
|
224
221
|
const formatXAxisLabelCallback = useCallback((timestamp) => formatXAxisLabel(timestamp, duration), [duration]);
|
|
225
|
-
return (_jsxs(LineTemporalChartWrapper, { children: [_jsxs(
|
|
222
|
+
return (_jsxs(LineTemporalChartWrapper, { children: [_jsxs(Stack, { gap: "r4", children: [_jsxs(ChartTitleText, { children: [title, " ", unitLabel && `(${unitLabel})`] }), helpText && (_jsx(IconHelp, { tooltipMessage: helpText, overlayStyle: maxWidthTooltip })), isLoading && _jsx(Loader, {})] }), _jsx("div", { onFocus: () => setIsChartActive(true), onBlur: () => setIsChartActive(false), onFocusCapture: () => setIsChartActive(true), onBlurCapture: () => setIsChartActive(false), children: _jsx(StyledResponsiveContainer, { width: "100%", height: height, children: _jsxs(LineChart, { data: rechartsData, ref: chartRef, margin: { top: 0, right: 0, bottom: 0, left: 0 }, "aria-label": `Time series chart for ${title}`, syncId: syncId, onMouseEnter: () => setIsChartActive(true), onMouseLeave: () => setIsChartActive(false), accessibilityLayer: true, children: [_jsx(CartesianGrid, { vertical: true, horizontal: true, verticalPoints: [0], horizontalPoints: [0], stroke: theme.border, fill: theme.backgroundLevel4, strokeWidth: 1 }), _jsx(XAxis, { dataKey: "timestamp", type: "number", domain: ['dataMin', 'dataMax'], ticks: xAxisTicks, tickFormatter: formatXAxisLabelCallback, tickCount: 5, tick: {
|
|
226
223
|
fill: theme.textSecondary,
|
|
227
224
|
fontSize: fontSize.smaller,
|
|
228
225
|
}, axisLine: { stroke: theme.border } }), _jsx(YAxis, { orientation: "right", label: {
|
|
229
226
|
value: yAxisTitle,
|
|
230
227
|
angle: 90,
|
|
231
|
-
|
|
228
|
+
dx: 20,
|
|
232
229
|
style: {
|
|
233
|
-
textAnchor: 'middle',
|
|
234
230
|
fill: theme.textSecondary,
|
|
235
231
|
fontSize: fontSize.smaller,
|
|
236
232
|
},
|
|
@@ -241,7 +237,7 @@ export function LineTimeSerieChart({ series, title, height, startingTimeStamp, i
|
|
|
241
237
|
: [0, topValue], axisLine: { stroke: theme.border }, tick: {
|
|
242
238
|
fill: theme.textSecondary,
|
|
243
239
|
fontSize: fontSize.smaller,
|
|
244
|
-
}, tickFormatter: (value) => new Intl.NumberFormat('fr-FR').format(value
|
|
240
|
+
}, tickFormatter: (value) => new Intl.NumberFormat('fr-FR').format(value), ticks: getTicks(topValue, yAxisType === 'symmetrical'), interval: 0 }), _jsx(Tooltip, { content: (props) => (_jsx(LineTimeSerieChartTooltip, { unitLabel: unitLabel, duration: duration, renderTooltip: renderTooltip, isSymmetrical: yAxisType === 'symmetrical', tooltipProps: props, isChartActive: isChartActive, hoveredValue: hoveredValue, chartContainerRef: chartRef })) }), yAxisType === 'symmetrical' && (_jsx(ReferenceLine, { y: 0, stroke: theme.border })), Object.entries(groupedSeries).map(([resource, resourceSeries]) => resourceSeries.map((serie, serieIndex) => {
|
|
245
241
|
const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
|
|
246
242
|
return (_jsx(Line, { type: "monotone", dataKey: label, stroke: colorMapping[resource], dot: false, isAnimationActive: false, strokeDasharray: serie.isLineDashed ? '4 4' : undefined, onMouseEnter: () => setHoveredValue(label), onMouseLeave: () => setHoveredValue(undefined) }, `${title}-${resource}-${serieIndex}`));
|
|
247
243
|
}))] }) }) })] }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TIME_FORMATER,
|
|
1
|
+
import { TIME_FORMATER, DAY_MONTH_ABBREVIATED_YEAR, DAY_MONTH_ABBREVIATED_HOUR_MINUTE, } from '../date/FormattedDateTime';
|
|
2
2
|
export const ONE_YEAR_MILLISECONDS = 366 * 24 * 60 * 60 * 1000;
|
|
3
3
|
/**
|
|
4
4
|
* Formats timestamp for X-axis labels based on time format and data range:
|
|
@@ -16,7 +16,7 @@ export const formatXAxisLabel = (timestamp, duration) => {
|
|
|
16
16
|
return TIME_FORMATER.format(date);
|
|
17
17
|
}
|
|
18
18
|
else if (duration <= 7 * 24 * 60 * 60) {
|
|
19
|
-
return
|
|
19
|
+
return DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(date)
|
|
20
20
|
.replace(',', '')
|
|
21
21
|
.replace(/Sept/g, 'Sep');
|
|
22
22
|
}
|
package/dist/style/theme.js
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Bar,
|
|
4
4
|
BarChart,
|
|
@@ -20,7 +20,7 @@ import { IconHelp } from '../iconhelper/IconHelper';
|
|
|
20
20
|
import { Loader } from '../loader/Loader.component';
|
|
21
21
|
import { Text } from '../text/Text.component';
|
|
22
22
|
import { BarchartTooltip } from './BarchartTooltip';
|
|
23
|
-
import { UnitRange, useChartData } from './utils';
|
|
23
|
+
import { getTicks, UnitRange, useChartData } from './utils';
|
|
24
24
|
|
|
25
25
|
const CHART_CONSTANTS = {
|
|
26
26
|
TICK_WIDTH_OFFSET: 4,
|
|
@@ -34,6 +34,7 @@ const CHART_CONSTANTS = {
|
|
|
34
34
|
bottom: 0,
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
+
const maxWidthTooltip = { maxWidth: '20rem' };
|
|
37
38
|
|
|
38
39
|
/* ---------------------------------- TYPE ---------------------------------- */
|
|
39
40
|
|
|
@@ -76,6 +77,7 @@ export type BarchartSortFn<T extends BarchartBars> = (
|
|
|
76
77
|
|
|
77
78
|
export type BarchartProps<T extends BarchartBars> = {
|
|
78
79
|
type: CategoryType | TimeType;
|
|
80
|
+
title: string;
|
|
79
81
|
bars?: T;
|
|
80
82
|
tooltip?: BarchartTooltipFn<T>;
|
|
81
83
|
defaultSort?: BarchartSortFn<T>;
|
|
@@ -89,7 +91,6 @@ export type BarchartProps<T extends BarchartBars> = {
|
|
|
89
91
|
* @default 'default'
|
|
90
92
|
*/
|
|
91
93
|
stackedBarSort?: 'default' | 'legend';
|
|
92
|
-
title?: string;
|
|
93
94
|
secondaryTitle?: string;
|
|
94
95
|
rightTitle?: React.ReactNode;
|
|
95
96
|
height?: number;
|
|
@@ -183,6 +184,7 @@ export const CustomTick = ({
|
|
|
183
184
|
export const StyledResponsiveContainer = styled(ResponsiveContainer)`
|
|
184
185
|
// Avoid tooltip over constrained text to be cut off
|
|
185
186
|
& .recharts-surface {
|
|
187
|
+
outline: none;
|
|
186
188
|
overflow: visible;
|
|
187
189
|
}
|
|
188
190
|
`;
|
|
@@ -202,7 +204,12 @@ const ChartHeader = ({
|
|
|
202
204
|
<Wrap>
|
|
203
205
|
<Stack gap="r4">
|
|
204
206
|
<Text variant="ChartTitle">{title}</Text>
|
|
205
|
-
{helpTooltip &&
|
|
207
|
+
{helpTooltip && (
|
|
208
|
+
<IconHelp
|
|
209
|
+
tooltipMessage={helpTooltip}
|
|
210
|
+
overlayStyle={maxWidthTooltip}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
206
213
|
|
|
207
214
|
{secondaryTitle && (
|
|
208
215
|
<Text
|
|
@@ -257,6 +264,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
|
|
|
257
264
|
const theme = useTheme();
|
|
258
265
|
const { getColor } = useChartLegend();
|
|
259
266
|
const [hoveredValue, setHoveredValue] = useState<string | undefined>();
|
|
267
|
+
const chartRef = useRef<HTMLDivElement>(null);
|
|
260
268
|
|
|
261
269
|
const {
|
|
262
270
|
height = CHART_CONSTANTS.DEFAULT_HEIGHT,
|
|
@@ -297,11 +305,11 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
|
|
|
297
305
|
unitRange,
|
|
298
306
|
stackedBarSort,
|
|
299
307
|
);
|
|
300
|
-
|
|
308
|
+
const titleWithUnit = unitLabel ? `${title} (${unitLabel})` : title;
|
|
301
309
|
return (
|
|
302
|
-
<Stack direction="vertical">
|
|
310
|
+
<Stack direction="vertical" style={{ gap: '0' }}>
|
|
303
311
|
<ChartHeader
|
|
304
|
-
title={
|
|
312
|
+
title={titleWithUnit}
|
|
305
313
|
secondaryTitle={secondaryTitle}
|
|
306
314
|
helpTooltip={helpTooltip}
|
|
307
315
|
rightTitle={rightTitle}
|
|
@@ -311,7 +319,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
|
|
|
311
319
|
) : isLoading ? (
|
|
312
320
|
<Loading height={height} />
|
|
313
321
|
) : (
|
|
314
|
-
<StyledResponsiveContainer width="100%" height={height}>
|
|
322
|
+
<StyledResponsiveContainer ref={chartRef} width="100%" height={height}>
|
|
315
323
|
<BarChart
|
|
316
324
|
data={rechartsData}
|
|
317
325
|
accessibilityLayer
|
|
@@ -352,13 +360,11 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
|
|
|
352
360
|
})}
|
|
353
361
|
|
|
354
362
|
<YAxis
|
|
355
|
-
tickCount={1}
|
|
356
363
|
interval={0}
|
|
357
|
-
unit={` ${unitLabel}`}
|
|
358
364
|
domain={[0, roundReferenceValue]}
|
|
365
|
+
ticks={getTicks(roundReferenceValue, false)}
|
|
359
366
|
tickFormatter={
|
|
360
|
-
(value) =>
|
|
361
|
-
new Intl.NumberFormat('fr-FR').format(value.toFixed(0)) // Add a space as thousand separator
|
|
367
|
+
(value) => new Intl.NumberFormat('fr-FR').format(value) // Add a space as thousand separator
|
|
362
368
|
}
|
|
363
369
|
axisLine={{ stroke: theme.border }}
|
|
364
370
|
tick={{
|
|
@@ -391,6 +397,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
|
|
|
391
397
|
hoveredValue={hoveredValue}
|
|
392
398
|
tooltip={tooltip}
|
|
393
399
|
unitLabel={unitLabel}
|
|
400
|
+
chartContainerRef={chartRef}
|
|
394
401
|
/>
|
|
395
402
|
)}
|
|
396
403
|
cursor={false}
|
|
@@ -19,6 +19,30 @@ const testTooltipProps = {
|
|
|
19
19
|
const testTooltip = () => <div>Test Tooltip</div>;
|
|
20
20
|
const date = new Date('2024-07-01T00:00:00').getTime();
|
|
21
21
|
|
|
22
|
+
// Create a mock DOM element for the chart container
|
|
23
|
+
const mockChartContainer = document.createElement('div');
|
|
24
|
+
mockChartContainer.getBoundingClientRect = jest.fn(() => ({
|
|
25
|
+
width: 800,
|
|
26
|
+
height: 400,
|
|
27
|
+
top: 0,
|
|
28
|
+
left: 0,
|
|
29
|
+
right: 800,
|
|
30
|
+
bottom: 400,
|
|
31
|
+
x: 0,
|
|
32
|
+
y: 0,
|
|
33
|
+
toJSON: () => ({
|
|
34
|
+
width: 800,
|
|
35
|
+
height: 400,
|
|
36
|
+
top: 0,
|
|
37
|
+
left: 0,
|
|
38
|
+
right: 800,
|
|
39
|
+
bottom: 400,
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
const mockChartContainerRef = { current: mockChartContainer };
|
|
45
|
+
|
|
22
46
|
describe('ChartTooltip', () => {
|
|
23
47
|
const selectors = {
|
|
24
48
|
tooltip: () => screen.queryByText(/Test Tooltip/),
|
|
@@ -39,6 +63,7 @@ describe('ChartTooltip', () => {
|
|
|
39
63
|
tooltipProps={testTooltipProps}
|
|
40
64
|
hoveredValue="Success"
|
|
41
65
|
tooltip={undefined}
|
|
66
|
+
chartContainerRef={mockChartContainerRef}
|
|
42
67
|
/>,
|
|
43
68
|
);
|
|
44
69
|
expect(selectors.category()).toBeInTheDocument();
|
|
@@ -54,6 +79,7 @@ describe('ChartTooltip', () => {
|
|
|
54
79
|
tooltipProps={testTooltipProps}
|
|
55
80
|
hoveredValue="Success"
|
|
56
81
|
tooltip={testTooltip}
|
|
82
|
+
chartContainerRef={mockChartContainerRef}
|
|
57
83
|
/>,
|
|
58
84
|
);
|
|
59
85
|
expect(selectors.tooltip()).toBeInTheDocument();
|
|
@@ -65,6 +91,7 @@ describe('ChartTooltip', () => {
|
|
|
65
91
|
tooltipProps={{ ...testTooltipProps, active: false }}
|
|
66
92
|
hoveredValue="Success"
|
|
67
93
|
tooltip={testTooltip}
|
|
94
|
+
chartContainerRef={mockChartContainerRef}
|
|
68
95
|
/>,
|
|
69
96
|
);
|
|
70
97
|
expect(selectors.tooltip()).not.toBeInTheDocument();
|
|
@@ -84,6 +111,7 @@ describe('ChartTooltip', () => {
|
|
|
84
111
|
}}
|
|
85
112
|
tooltipProps={{ ...testTooltipProps, label }}
|
|
86
113
|
hoveredValue="Success"
|
|
114
|
+
chartContainerRef={mockChartContainerRef}
|
|
87
115
|
/>,
|
|
88
116
|
);
|
|
89
117
|
expect(selectors.success()).toBeInTheDocument();
|
|
@@ -107,6 +135,7 @@ describe('ChartTooltip', () => {
|
|
|
107
135
|
}}
|
|
108
136
|
tooltipProps={{ ...testTooltipProps, label }}
|
|
109
137
|
hoveredValue="Success"
|
|
138
|
+
chartContainerRef={mockChartContainerRef}
|
|
110
139
|
/>,
|
|
111
140
|
);
|
|
112
141
|
expect(selectors.success()).toBeInTheDocument();
|
|
@@ -133,6 +162,7 @@ describe('ChartTooltip', () => {
|
|
|
133
162
|
tooltipProps={tooltipProps}
|
|
134
163
|
hoveredValue="Success"
|
|
135
164
|
unitLabel="kB"
|
|
165
|
+
chartContainerRef={mockChartContainerRef}
|
|
136
166
|
/>,
|
|
137
167
|
);
|
|
138
168
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TooltipContentProps } from 'recharts';
|
|
2
2
|
import { LegendShape } from '../chartlegend/ChartLegend';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
ChartTooltipPortal,
|
|
5
5
|
ChartTooltipHeader,
|
|
6
6
|
ChartTooltipItem,
|
|
7
7
|
ChartTooltipItemsContainer,
|
|
@@ -22,6 +22,7 @@ export const BarchartTooltip = <T extends BarchartBars>({
|
|
|
22
22
|
hoveredValue,
|
|
23
23
|
tooltip,
|
|
24
24
|
unitLabel,
|
|
25
|
+
chartContainerRef,
|
|
25
26
|
}: {
|
|
26
27
|
type: TimeType | CategoryType;
|
|
27
28
|
tooltipProps: TooltipContentProps<number, string>;
|
|
@@ -29,23 +30,25 @@ export const BarchartTooltip = <T extends BarchartBars>({
|
|
|
29
30
|
hoveredValue: string | undefined;
|
|
30
31
|
tooltip?: BarchartTooltipFn<T>;
|
|
31
32
|
unitLabel?: string;
|
|
33
|
+
chartContainerRef: React.RefObject<HTMLDivElement>;
|
|
32
34
|
}) => {
|
|
33
|
-
const { active } = tooltipProps;
|
|
35
|
+
const { active, coordinate } = tooltipProps;
|
|
34
36
|
|
|
35
37
|
if (!active) {
|
|
36
38
|
return null;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const currentPoint = getCurrentPoint(tooltipProps, hoveredValue);
|
|
40
|
-
|
|
41
|
-
return tooltip(currentPoint);
|
|
42
|
-
}
|
|
42
|
+
|
|
43
43
|
const duration =
|
|
44
44
|
type.type === 'time'
|
|
45
45
|
? type.timeRange.startDate.getTime() - type.timeRange.endDate.getTime()
|
|
46
46
|
: 0;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
const tooltipContent = tooltip ? (
|
|
49
|
+
tooltip(currentPoint)
|
|
50
|
+
) : (
|
|
51
|
+
<>
|
|
49
52
|
<ChartTooltipHeader>
|
|
50
53
|
{type.type === 'time' ? (
|
|
51
54
|
<TooltipHeader duration={duration} value={currentPoint.category} />
|
|
@@ -80,6 +83,16 @@ export const BarchartTooltip = <T extends BarchartBars>({
|
|
|
80
83
|
);
|
|
81
84
|
})}
|
|
82
85
|
</ChartTooltipItemsContainer>
|
|
83
|
-
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ChartTooltipPortal
|
|
91
|
+
coordinate={coordinate}
|
|
92
|
+
chartContainerRef={chartContainerRef}
|
|
93
|
+
isVisible={active}
|
|
94
|
+
>
|
|
95
|
+
{tooltipContent}
|
|
96
|
+
</ChartTooltipPortal>
|
|
84
97
|
);
|
|
85
98
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getCurrentPoint,
|
|
8
8
|
getMaxBarValue,
|
|
9
9
|
getRoundReferenceValue,
|
|
10
|
+
getTicks,
|
|
10
11
|
sortStackedBars,
|
|
11
12
|
transformCategoryData,
|
|
12
13
|
transformTimeData,
|
|
@@ -504,21 +505,73 @@ describe('applySortingToData', () => {
|
|
|
504
505
|
});
|
|
505
506
|
|
|
506
507
|
describe('getRoundReferenceValue', () => {
|
|
507
|
-
it('should return appropriate rounded values', () => {
|
|
508
|
-
|
|
509
|
-
expect(getRoundReferenceValue(
|
|
510
|
-
expect(getRoundReferenceValue(
|
|
511
|
-
expect(getRoundReferenceValue(
|
|
512
|
-
expect(getRoundReferenceValue(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
expect(getRoundReferenceValue(
|
|
516
|
-
expect(getRoundReferenceValue(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
expect(getRoundReferenceValue(
|
|
520
|
-
expect(getRoundReferenceValue(
|
|
521
|
-
expect(getRoundReferenceValue(
|
|
508
|
+
it('should return appropriate rounded values with 10% buffer', () => {
|
|
509
|
+
// Small values (< 10)
|
|
510
|
+
expect(getRoundReferenceValue(0.1)).toBe(0.2); // 0.1 → 0.11 → 0.2
|
|
511
|
+
expect(getRoundReferenceValue(1)).toBe(2); // 1.1 → 1.5 → 2
|
|
512
|
+
expect(getRoundReferenceValue(2)).toBe(5); // 2.2 → 3 → 5
|
|
513
|
+
expect(getRoundReferenceValue(3)).toBe(5); // 3.3 → 4 → 5
|
|
514
|
+
|
|
515
|
+
// Values 5-10 range
|
|
516
|
+
expect(getRoundReferenceValue(6)).toBe(10); // 6.6 → 10 (skip 7.5 for values < 10)
|
|
517
|
+
expect(getRoundReferenceValue(9)).toBe(10); // 9.9 → 10
|
|
518
|
+
|
|
519
|
+
// Larger values get 10% buffer applied
|
|
520
|
+
expect(getRoundReferenceValue(15)).toBe(20); // 16.5 → 20
|
|
521
|
+
expect(getRoundReferenceValue(35)).toBe(40); // 38.5 → 40
|
|
522
|
+
expect(getRoundReferenceValue(75)).toBe(100); // 82.5 → 100
|
|
523
|
+
expect(getRoundReferenceValue(150)).toBe(200); // 165 → 200
|
|
524
|
+
expect(getRoundReferenceValue(350)).toBe(400); // 385 → 400
|
|
525
|
+
expect(getRoundReferenceValue(750)).toBe(1000); // 825 → 1000
|
|
526
|
+
expect(getRoundReferenceValue(1500)).toBe(2000); // 1650 → 2000
|
|
527
|
+
expect(getRoundReferenceValue(3500)).toBe(4000); // 3850 → 4000
|
|
528
|
+
expect(getRoundReferenceValue(7500)).toBe(10000); // 8250 → 10000
|
|
529
|
+
expect(getRoundReferenceValue(15000)).toBe(20000); // 16500 → 20000
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('getTicks', () => {
|
|
534
|
+
describe('small values (< 10)', () => {
|
|
535
|
+
it('should return 2 ticks for non-symmetrical small values', () => {
|
|
536
|
+
expect(getTicks(1, false)).toEqual([0, 1]);
|
|
537
|
+
expect(getTicks(2, false)).toEqual([0, 2]);
|
|
538
|
+
expect(getTicks(5, false)).toEqual([0, 5]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should return 3 ticks for symmetrical small values', () => {
|
|
542
|
+
expect(getTicks(1, true)).toEqual([-1, 0, 1]);
|
|
543
|
+
expect(getTicks(2, true)).toEqual([-2, 0, 2]);
|
|
544
|
+
expect(getTicks(5, true)).toEqual([-5, 0, 5]);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe('even topValue (divisible by 2)', () => {
|
|
549
|
+
it('should return 3 ticks for non-symmetrical even values', () => {
|
|
550
|
+
expect(getTicks(10, false)).toEqual([0, 5, 10]);
|
|
551
|
+
expect(getTicks(20, false)).toEqual([0, 10, 20]);
|
|
552
|
+
expect(getTicks(40, false)).toEqual([0, 20, 40]);
|
|
553
|
+
expect(getTicks(50, false)).toEqual([0, 25, 50]);
|
|
554
|
+
expect(getTicks(100, false)).toEqual([0, 50, 100]);
|
|
555
|
+
expect(getTicks(1000, false)).toEqual([0, 500, 1000]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should return 5 ticks for symmetrical even values', () => {
|
|
559
|
+
expect(getTicks(10, true)).toEqual([-10, -5, 0, 5, 10]);
|
|
560
|
+
expect(getTicks(100, true)).toEqual([-100, -50, 0, 50, 100]);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe('odd topValue (not divisible by 2) - 7.5 multiples', () => {
|
|
565
|
+
it('should return 4 ticks for non-symmetrical values from 7.5 × magnitude', () => {
|
|
566
|
+
expect(getTicks(75, false)).toEqual([0, 25, 50, 75]);
|
|
567
|
+
expect(getTicks(750, false)).toEqual([0, 250, 500, 750]);
|
|
568
|
+
expect(getTicks(7500, false)).toEqual([0, 2500, 5000, 7500]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should return 7 ticks for symmetrical values from 7.5 × magnitude', () => {
|
|
572
|
+
expect(getTicks(75, true)).toEqual([-75, -50, -25, 0, 25, 50, 75]);
|
|
573
|
+
expect(getTicks(750, true)).toEqual([-750, -500, -250, 0, 250, 500, 750]);
|
|
574
|
+
});
|
|
522
575
|
});
|
|
523
576
|
});
|
|
524
577
|
|
|
@@ -685,7 +738,8 @@ describe('computeUnitLabelAndRoundReferenceValue', () => {
|
|
|
685
738
|
);
|
|
686
739
|
|
|
687
740
|
expect(result.unitLabel).toBe('kB');
|
|
688
|
-
|
|
741
|
+
// 1680 / 1000 = 1.68, with buffer: 1.848 → rounds to 2
|
|
742
|
+
expect(result.roundReferenceValue).toBe(2);
|
|
689
743
|
expect(result.rechartsData).toEqual([
|
|
690
744
|
{
|
|
691
745
|
category: 'category1',
|
|
@@ -718,7 +772,8 @@ describe('computeUnitLabelAndRoundReferenceValue', () => {
|
|
|
718
772
|
);
|
|
719
773
|
|
|
720
774
|
expect(result.unitLabel).toBe('B');
|
|
721
|
-
|
|
775
|
+
// 680 with buffer: 748 → rounds to 750 (7.5 * 100, value > 10)
|
|
776
|
+
expect(result.roundReferenceValue).toBe(750);
|
|
722
777
|
expect(result.rechartsData).toEqual([
|
|
723
778
|
{ category: 'category1', success: 680 },
|
|
724
779
|
]);
|