@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.
Files changed (43) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts +1 -1
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +10 -6
  4. package/dist/components/barchartv2/BarchartTooltip.d.ts +3 -2
  5. package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -1
  6. package/dist/components/barchartv2/BarchartTooltip.js +6 -8
  7. package/dist/components/barchartv2/utils.d.ts +6 -1
  8. package/dist/components/barchartv2/utils.d.ts.map +1 -1
  9. package/dist/components/barchartv2/utils.js +34 -8
  10. package/dist/components/charttooltip/ChartTooltip.d.ts +23 -0
  11. package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -1
  12. package/dist/components/charttooltip/ChartTooltip.js +83 -1
  13. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -1
  14. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +27 -1
  15. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +1 -1
  16. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -1
  17. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +19 -59
  18. package/dist/components/globalhealthbar/components/HealthBarXAxis.js +1 -1
  19. package/dist/components/globalhealthbar/useHealthBarData.d.ts +1 -0
  20. package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -1
  21. package/dist/components/globalhealthbar/useHealthBarData.js +1 -0
  22. package/dist/components/globalhealthbar/useHealthBarData.spec.js +2 -0
  23. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  24. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +43 -47
  25. package/dist/components/linetimeseriechart/utils.js +2 -2
  26. package/dist/style/theme.js +1 -1
  27. package/package.json +1 -1
  28. package/src/lib/components/barchartv2/Barchart.component.tsx +19 -12
  29. package/src/lib/components/barchartv2/BarchartTooltip.test.tsx +30 -0
  30. package/src/lib/components/barchartv2/BarchartTooltip.tsx +21 -8
  31. package/src/lib/components/barchartv2/utils.test.ts +72 -17
  32. package/src/lib/components/barchartv2/utils.ts +39 -7
  33. package/src/lib/components/charttooltip/ChartTooltip.tsx +134 -1
  34. package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +56 -11
  35. package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +75 -117
  36. package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +1 -1
  37. package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +2 -0
  38. package/src/lib/components/globalhealthbar/useHealthBarData.ts +2 -0
  39. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +86 -82
  40. package/src/lib/components/linetimeseriechart/utils.test.ts +3 -3
  41. package/src/lib/components/linetimeseriechart/utils.ts +2 -2
  42. package/src/lib/style/theme.ts +1 -1
  43. 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;CAClD;AAED,eAAO,MAAM,gBAAgB,WACnB,KAAK,EAAE,kBACC,MAAM,gBACR,MAAM,MAChB,MAAM;;;;;;;;;;CA4DX,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;AAqCtE,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;AAkGF,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,2CA4WhB"}
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 { spacing } from '../../spacing';
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, Text } from '../text/Text.component';
13
- import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component';
10
+ import { ChartTitleText } from '../text/Text.component';
14
11
  import { formatXAxisLabel } from './utils';
15
- import { ChartTooltipContainer, ChartTooltipItem, ChartTooltipHeader, ChartTooltipItemsContainer, ChartTooltipSeparator, TooltipHeader, } from '../charttooltip/ChartTooltip';
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 ChartHeader = styled.div `
24
- display: flex;
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
- if (renderTooltip) {
32
- return renderTooltip(tooltipProps, unitLabel, duration);
33
- }
34
- // We can't use the default itemSorter method because it's a custom tooltip.
35
- // Sort the payload here instead
36
- const sortedPayload = [...payload].sort((a, b) => {
37
- const aValue = a.value;
38
- const bValue = b.value;
39
- if (aValue >= 0 && bValue >= 0) {
40
- return bValue - aValue; // Higher positive values first
41
- }
42
- if (aValue < 0 && bValue < 0) {
43
- return bValue - aValue; // Lower negative values first
44
- }
45
- return bValue - aValue; // Positives before negatives
46
- });
47
- // Find the transition point between positive and negative values
48
- const separatorIndex = sortedPayload.findIndex((entry) => entry.value < 0);
49
- const hasBothPositiveAndNegative = separatorIndex > 0 && separatorIndex < sortedPayload.length;
50
- return (_jsxs(ChartTooltipContainer, { children: [_jsx(ChartTooltipHeader, { children: _jsx(TooltipHeader, { duration: duration, value: label }) }), _jsx(ChartTooltipItemsContainer, { children: sortedPayload.map((entry, index) => {
51
- const legendIcon = (_jsx(LegendShape, { color: entry.color, shape: "line", chartColors: { [entry.color]: entry.color } }));
52
- const isHovered = entry.name === hoveredValue;
53
- const formattedValue = !Number.isFinite(entry.value)
54
- ? '-'
55
- : `${entry.value.toFixed(2)} ${unitLabel}`;
56
- return (_jsxs(React.Fragment, { children: [isSymmetrical &&
57
- hasBothPositiveAndNegative &&
58
- index === separatorIndex && _jsx(ChartTooltipSeparator, {}), _jsx(ChartTooltipItem, { label: entry.name, value: formattedValue, legendIcon: legendIcon, isHovered: isHovered })] }, index));
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
- const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
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(ChartHeader, { children: [_jsxs(ChartTitleText, { children: [title, " ", unitLabel && `(${unitLabel})`] }), helpText && (_jsx(Box, { ml: spacing.r4, children: _jsx(TooltipComponent, { placement: 'right', overlay: _jsx(Text, { children: helpText }), children: _jsx(Icon, { name: "Info", color: theme.buttonSecondary }) }) })), 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: {
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
- position: 'insideRight',
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.toFixed(0)), tickCount: 5, interval: 0 }), _jsx(Tooltip, { content: (props) => (_jsx(LineTimeSerieChartTooltip, { unitLabel: unitLabel, duration: duration, renderTooltip: renderTooltip, isSymmetrical: yAxisType === 'symmetrical', tooltipProps: props, isChartActive: isChartActive, hoveredValue: hoveredValue })) }), yAxisType === 'symmetrical' && (_jsx(ReferenceLine, { y: 0, stroke: theme.border })), Object.entries(groupedSeries).map(([resource, resourceSeries]) => resourceSeries.map((serie, serieIndex) => {
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, DAY_MONTH_ABBREVIATED, DAY_MONTH_ABBREVIATED_YEAR, } from '../date/FormattedDateTime';
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 DAY_MONTH_ABBREVIATED.format(date)
19
+ return DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(date)
20
20
  .replace(',', '')
21
21
  .replace(/Sept/g, 'Sep');
22
22
  }
@@ -33,7 +33,7 @@ export const coreUIAvailableThemesNames = [
33
33
  'darkRebrand',
34
34
  'artescaLight',
35
35
  'ring9dark',
36
- 'G-Dark'
36
+ 'G-Dark',
37
37
  ];
38
38
  export const coreUIAvailableThemes = {
39
39
  darkRebrand: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/core-ui",
3
- "version": "0.176.0",
3
+ "version": "0.177.0",
4
4
  "description": "Scality common React component library",
5
5
  "author": "Scality Engineering",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -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 && <IconHelp tooltipMessage={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={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
- ChartTooltipContainer,
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
- if (tooltip) {
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
- return (
48
- <ChartTooltipContainer>
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
- </ChartTooltipContainer>
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
- expect(getRoundReferenceValue(1)).toBe(5);
509
- expect(getRoundReferenceValue(2)).toBe(5);
510
- expect(getRoundReferenceValue(3)).toBe(5);
511
- expect(getRoundReferenceValue(7)).toBe(10);
512
- expect(getRoundReferenceValue(15)).toBe(25);
513
- expect(getRoundReferenceValue(35)).toBe(50);
514
- expect(getRoundReferenceValue(75)).toBe(100);
515
- expect(getRoundReferenceValue(150)).toBe(250);
516
- expect(getRoundReferenceValue(350)).toBe(500);
517
- expect(getRoundReferenceValue(750)).toBe(1000);
518
- expect(getRoundReferenceValue(1500)).toBe(2500);
519
- expect(getRoundReferenceValue(3500)).toBe(5000);
520
- expect(getRoundReferenceValue(7500)).toBe(10000);
521
- expect(getRoundReferenceValue(15000)).toBe(25000);
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
- expect(result.roundReferenceValue).toBe(10);
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
- expect(result.roundReferenceValue).toBe(1000);
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
  ]);