@scality/core-ui 0.175.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 (55) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts +4 -5
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +23 -27
  4. package/dist/components/barchartv2/BarchartTooltip.d.ts +4 -3
  5. package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -1
  6. package/dist/components/barchartv2/BarchartTooltip.js +10 -12
  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/chartlegend/ChartLegendWrapper.js +1 -1
  11. package/dist/components/charttooltip/ChartTooltip.d.ts +29 -0
  12. package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -1
  13. package/dist/components/charttooltip/ChartTooltip.js +105 -1
  14. package/dist/components/date/FormattedDateTime.d.ts +23 -8
  15. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  16. package/dist/components/date/FormattedDateTime.js +51 -7
  17. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -1
  18. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +27 -1
  19. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +1 -1
  20. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -1
  21. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +19 -59
  22. package/dist/components/globalhealthbar/components/HealthBarXAxis.js +1 -1
  23. package/dist/components/globalhealthbar/useHealthBarData.d.ts +1 -0
  24. package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -1
  25. package/dist/components/globalhealthbar/useHealthBarData.js +1 -0
  26. package/dist/components/globalhealthbar/useHealthBarData.spec.js +2 -0
  27. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +2 -1
  28. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  29. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +45 -47
  30. package/dist/components/linetimeseriechart/utils.d.ts +1 -1
  31. package/dist/components/linetimeseriechart/utils.d.ts.map +1 -1
  32. package/dist/components/linetimeseriechart/utils.js +13 -13
  33. package/dist/style/theme.js +1 -1
  34. package/package.json +1 -1
  35. package/src/lib/components/barchartv2/Barchart.component.test.tsx +23 -25
  36. package/src/lib/components/barchartv2/Barchart.component.tsx +41 -39
  37. package/src/lib/components/barchartv2/BarchartTooltip.test.tsx +33 -3
  38. package/src/lib/components/barchartv2/BarchartTooltip.tsx +33 -24
  39. package/src/lib/components/barchartv2/utils.test.ts +72 -17
  40. package/src/lib/components/barchartv2/utils.ts +40 -12
  41. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +1 -1
  42. package/src/lib/components/charttooltip/ChartTooltip.tsx +174 -1
  43. package/src/lib/components/date/FormattedDateTime.tsx +73 -8
  44. package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +56 -11
  45. package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +75 -117
  46. package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +1 -1
  47. package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +2 -0
  48. package/src/lib/components/globalhealthbar/useHealthBarData.ts +2 -0
  49. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +101 -90
  50. package/src/lib/components/linetimeseriechart/utils.test.ts +30 -68
  51. package/src/lib/components/linetimeseriechart/utils.ts +13 -17
  52. package/src/lib/style/theme.ts +1 -1
  53. package/stories/BarChart/barchart.stories.tsx +23 -8
  54. package/stories/formattedate.stories.tsx +2 -0
  55. package/stories/linetimeseriechart.stories.tsx +1 -0
@@ -89,9 +89,9 @@ describe('Barchart', () => {
89
89
  </Wrapper>,
90
90
  );
91
91
 
92
- expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
93
- expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
94
- expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
92
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
93
+ expect(screen.getByText('06 Jul')).toBeInTheDocument();
94
+ expect(screen.getByText('07 Jul')).toBeInTheDocument();
95
95
  });
96
96
  it('should render the Barchart component with error state', async () => {
97
97
  const { Wrapper } = getWrapper();
@@ -153,11 +153,11 @@ describe('Barchart', () => {
153
153
  </ChartLegendWrapper>
154
154
  </Wrapper>,
155
155
  );
156
- expect(screen.getByText('Wed03Jul')).toBeInTheDocument();
157
- expect(screen.getByText('Thu04Jul')).toBeInTheDocument();
158
- expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
159
- expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
160
- expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
156
+ expect(screen.getByText('03 Jul')).toBeInTheDocument();
157
+ expect(screen.getByText('04 Jul')).toBeInTheDocument();
158
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
159
+ expect(screen.getByText('06 Jul')).toBeInTheDocument();
160
+ expect(screen.getByText('07 Jul')).toBeInTheDocument();
161
161
  });
162
162
  it('should render when there are missing data in the time range', async () => {
163
163
  const bars = [
@@ -203,10 +203,10 @@ describe('Barchart', () => {
203
203
 
204
204
  // Check that all days are present
205
205
  await waitFor(() => {
206
- expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
207
- expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
208
- expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
209
- expect(screen.getByText('Mon08Jul')).toBeInTheDocument();
206
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
207
+ expect(screen.getByText('06 Jul')).toBeInTheDocument();
208
+ expect(screen.getByText('07 Jul')).toBeInTheDocument();
209
+ expect(screen.getByText('08 Jul')).toBeInTheDocument();
210
210
  });
211
211
  });
212
212
  it('should render for a specific time range', async () => {
@@ -244,13 +244,13 @@ describe('Barchart', () => {
244
244
  </Wrapper>,
245
245
  );
246
246
  await waitFor(() => {
247
- expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
248
- expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
249
- expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
250
- expect(screen.getByText('Mon08Jul')).toBeInTheDocument();
251
- expect(screen.getByText('Tue09Jul')).toBeInTheDocument();
252
- expect(screen.getByText('Wed10Jul')).toBeInTheDocument();
253
- expect(screen.getByText('Thu11Jul')).toBeInTheDocument();
247
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
248
+ expect(screen.getByText('06 Jul')).toBeInTheDocument();
249
+ expect(screen.getByText('07 Jul')).toBeInTheDocument();
250
+ expect(screen.getByText('08 Jul')).toBeInTheDocument();
251
+ expect(screen.getByText('09 Jul')).toBeInTheDocument();
252
+ expect(screen.getByText('10 Jul')).toBeInTheDocument();
253
+ expect(screen.getByText('11 Jul')).toBeInTheDocument();
254
254
  });
255
255
  });
256
256
  it('should render the Barchart component with hourly intervals', async () => {
@@ -411,7 +411,7 @@ describe('Barchart', () => {
411
411
  />
412
412
  </Wrapper>,
413
413
  );
414
- expect(screen.getByText('Fri05Jul 10:00')).toBeInTheDocument();
414
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
415
415
  });
416
416
 
417
417
  it('should render the CustomTick component with day format', () => {
@@ -435,7 +435,7 @@ describe('Barchart', () => {
435
435
  />
436
436
  </Wrapper>,
437
437
  );
438
- expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
438
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
439
439
  });
440
440
  it('should render the CustomTick component with hour format', () => {
441
441
  const { Wrapper } = getWrapper();
@@ -458,7 +458,7 @@ describe('Barchart', () => {
458
458
  />
459
459
  </Wrapper>,
460
460
  );
461
- expect(screen.getByText('10:00')).toBeInTheDocument();
461
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
462
462
  });
463
463
  it('should render the CustomTick component with minute format', () => {
464
464
  const { Wrapper } = getWrapper();
@@ -481,9 +481,7 @@ describe('Barchart', () => {
481
481
  />
482
482
  </Wrapper>,
483
483
  );
484
- expect(
485
- screen.getByText(new Date('2024-07-05T10:00:00').getTime()),
486
- ).toBeInTheDocument();
484
+ expect(screen.getByText('05 Jul')).toBeInTheDocument();
487
485
  });
488
486
  });
489
487
  });
@@ -1,9 +1,8 @@
1
- import { useState } from 'react';
1
+ import { useState, useRef } from 'react';
2
2
  import {
3
3
  Bar,
4
4
  BarChart,
5
5
  CartesianGrid,
6
- ReferenceLine,
7
6
  ResponsiveContainer,
8
7
  Tooltip,
9
8
  TooltipContentProps,
@@ -21,7 +20,7 @@ import { IconHelp } from '../iconhelper/IconHelper';
21
20
  import { Loader } from '../loader/Loader.component';
22
21
  import { Text } from '../text/Text.component';
23
22
  import { BarchartTooltip } from './BarchartTooltip';
24
- import { UnitRange, useChartData } from './utils';
23
+ import { getTicks, UnitRange, useChartData } from './utils';
25
24
 
26
25
  const CHART_CONSTANTS = {
27
26
  TICK_WIDTH_OFFSET: 4,
@@ -35,6 +34,7 @@ const CHART_CONSTANTS = {
35
34
  bottom: 0,
36
35
  },
37
36
  };
37
+ const maxWidthTooltip = { maxWidth: '20rem' };
38
38
 
39
39
  /* ---------------------------------- TYPE ---------------------------------- */
40
40
 
@@ -77,6 +77,7 @@ export type BarchartSortFn<T extends BarchartBars> = (
77
77
 
78
78
  export type BarchartProps<T extends BarchartBars> = {
79
79
  type: CategoryType | TimeType;
80
+ title: string;
80
81
  bars?: T;
81
82
  tooltip?: BarchartTooltipFn<T>;
82
83
  defaultSort?: BarchartSortFn<T>;
@@ -90,7 +91,6 @@ export type BarchartProps<T extends BarchartBars> = {
90
91
  * @default 'default'
91
92
  */
92
93
  stackedBarSort?: 'default' | 'legend';
93
- title?: string;
94
94
  secondaryTitle?: string;
95
95
  rightTitle?: React.ReactNode;
96
96
  height?: number;
@@ -112,33 +112,19 @@ interface CustomTickProps {
112
112
  /* ---------------------------------- COMPONENTS ---------------------------------- */
113
113
 
114
114
  /**
115
- * Formats a date based on the interval
116
- * @param timestamp - Timestamp
117
- * @param interval - Interval in milliseconds
115
+ * Get the format of the date based on the duration
116
+ * @param duration - Duration in milliseconds
118
117
  * @returns Formatted string
119
118
  */
120
119
  export const formatDate = (
121
- timestamp: number,
122
- interval: number,
123
- ): React.ReactNode => {
124
- const date = new Date(timestamp);
125
- // More than 24 hours interval - use day and time format
126
- if (interval > 24 * 60 * 60 * 1000) {
127
- return (
128
- <>
129
- <FormattedDateTime format="chart-date" value={date} />{' '}
130
- <FormattedDateTime format="time" value={date} />
131
- </>
132
- );
133
- } else if (interval === 24 * 60 * 60 * 1000) {
134
- // Daily interval - use day format
135
- return <FormattedDateTime format="chart-date" value={date} />;
136
- } else if (interval >= 60 * 1000) {
137
- //Hourly and minute intervals - use minute format
138
- return <FormattedDateTime format="time" value={date} />;
120
+ duration: number,
121
+ ): 'time' | 'day-month-abbreviated' | 'chart-long-term-date' => {
122
+ if (duration <= 24 * 60 * 60 * 1000) {
123
+ return 'time';
124
+ } else if (duration <= 7 * 24 * 60 * 60 * 1000) {
125
+ return 'day-month-abbreviated';
139
126
  } else {
140
- // minute interval or less - use full timestamp
141
- return timestamp;
127
+ return 'chart-long-term-date';
142
128
  }
143
129
  };
144
130
 
@@ -155,6 +141,11 @@ export const CustomTick = ({
155
141
  width / visibleTicksCount - CHART_CONSTANTS.TICK_WIDTH_OFFSET;
156
142
  const centerX = x - tickWidth / 2;
157
143
 
144
+ const duration =
145
+ type.type === 'time'
146
+ ? type.timeRange.endDate.getTime() - type.timeRange.startDate.getTime()
147
+ : 0;
148
+
158
149
  return (
159
150
  <foreignObject
160
151
  x={centerX}
@@ -167,9 +158,14 @@ export const CustomTick = ({
167
158
  color="textSecondary"
168
159
  text={
169
160
  <Text variant="Smaller">
170
- {type.type === 'time'
171
- ? formatDate(payload.value, type.timeRange.interval)
172
- : String(payload.value)}
161
+ {type.type === 'time' ? (
162
+ <FormattedDateTime
163
+ format={formatDate(duration)}
164
+ value={new Date(payload.value)}
165
+ />
166
+ ) : (
167
+ String(payload.value)
168
+ )}
173
169
  </Text>
174
170
  }
175
171
  centered
@@ -188,6 +184,7 @@ export const CustomTick = ({
188
184
  export const StyledResponsiveContainer = styled(ResponsiveContainer)`
189
185
  // Avoid tooltip over constrained text to be cut off
190
186
  & .recharts-surface {
187
+ outline: none;
191
188
  overflow: visible;
192
189
  }
193
190
  `;
@@ -207,7 +204,12 @@ const ChartHeader = ({
207
204
  <Wrap>
208
205
  <Stack gap="r4">
209
206
  <Text variant="ChartTitle">{title}</Text>
210
- {helpTooltip && <IconHelp tooltipMessage={helpTooltip} />}
207
+ {helpTooltip && (
208
+ <IconHelp
209
+ tooltipMessage={helpTooltip}
210
+ overlayStyle={maxWidthTooltip}
211
+ />
212
+ )}
211
213
 
212
214
  {secondaryTitle && (
213
215
  <Text
@@ -262,6 +264,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
262
264
  const theme = useTheme();
263
265
  const { getColor } = useChartLegend();
264
266
  const [hoveredValue, setHoveredValue] = useState<string | undefined>();
267
+ const chartRef = useRef<HTMLDivElement>(null);
265
268
 
266
269
  const {
267
270
  height = CHART_CONSTANTS.DEFAULT_HEIGHT,
@@ -302,11 +305,11 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
302
305
  unitRange,
303
306
  stackedBarSort,
304
307
  );
305
-
308
+ const titleWithUnit = unitLabel ? `${title} (${unitLabel})` : title;
306
309
  return (
307
- <Stack direction="vertical">
310
+ <Stack direction="vertical" style={{ gap: '0' }}>
308
311
  <ChartHeader
309
- title={title}
312
+ title={titleWithUnit}
310
313
  secondaryTitle={secondaryTitle}
311
314
  helpTooltip={helpTooltip}
312
315
  rightTitle={rightTitle}
@@ -316,7 +319,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
316
319
  ) : isLoading ? (
317
320
  <Loading height={height} />
318
321
  ) : (
319
- <StyledResponsiveContainer width="100%" height={height}>
322
+ <StyledResponsiveContainer ref={chartRef} width="100%" height={height}>
320
323
  <BarChart
321
324
  data={rechartsData}
322
325
  accessibilityLayer
@@ -357,13 +360,11 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
357
360
  })}
358
361
 
359
362
  <YAxis
360
- tickCount={1}
361
363
  interval={0}
362
- unit={` ${unitLabel}`}
363
364
  domain={[0, roundReferenceValue]}
365
+ ticks={getTicks(roundReferenceValue, false)}
364
366
  tickFormatter={
365
- (value) =>
366
- 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
367
368
  }
368
369
  axisLine={{ stroke: theme.border }}
369
370
  tick={{
@@ -396,6 +397,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
396
397
  hoveredValue={hoveredValue}
397
398
  tooltip={tooltip}
398
399
  unitLabel={unitLabel}
400
+ chartContainerRef={chartRef}
399
401
  />
400
402
  )}
401
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/),
@@ -30,6 +54,7 @@ describe('ChartTooltip', () => {
30
54
  longDate: () => screen.queryByText(/01 July 2024/),
31
55
  date: () => screen.queryByText(/\b01 Jul\b/),
32
56
  time: () => screen.queryByText(/00:00:00/),
57
+ dateTime: () => screen.queryByText(/01 Jul 00:00:00/),
33
58
  };
34
59
  it('should render the BarchartTooltip component', () => {
35
60
  render(
@@ -38,6 +63,7 @@ describe('ChartTooltip', () => {
38
63
  tooltipProps={testTooltipProps}
39
64
  hoveredValue="Success"
40
65
  tooltip={undefined}
66
+ chartContainerRef={mockChartContainerRef}
41
67
  />,
42
68
  );
43
69
  expect(selectors.category()).toBeInTheDocument();
@@ -53,6 +79,7 @@ describe('ChartTooltip', () => {
53
79
  tooltipProps={testTooltipProps}
54
80
  hoveredValue="Success"
55
81
  tooltip={testTooltip}
82
+ chartContainerRef={mockChartContainerRef}
56
83
  />,
57
84
  );
58
85
  expect(selectors.tooltip()).toBeInTheDocument();
@@ -64,6 +91,7 @@ describe('ChartTooltip', () => {
64
91
  tooltipProps={{ ...testTooltipProps, active: false }}
65
92
  hoveredValue="Success"
66
93
  tooltip={testTooltip}
94
+ chartContainerRef={mockChartContainerRef}
67
95
  />,
68
96
  );
69
97
  expect(selectors.tooltip()).not.toBeInTheDocument();
@@ -83,15 +111,15 @@ describe('ChartTooltip', () => {
83
111
  }}
84
112
  tooltipProps={{ ...testTooltipProps, label }}
85
113
  hoveredValue="Success"
114
+ chartContainerRef={mockChartContainerRef}
86
115
  />,
87
116
  );
88
117
  expect(selectors.success()).toBeInTheDocument();
89
118
  expect(selectors.successValue()).toBeInTheDocument();
90
119
  expect(selectors.failed()).toBeInTheDocument();
91
120
  expect(selectors.failedValue()).toBeInTheDocument();
92
- expect(selectors.date()).not.toBeInTheDocument();
93
- expect(selectors.longDate()).toBeInTheDocument();
94
- expect(selectors.time()).not.toBeInTheDocument();
121
+
122
+ expect(selectors.dateTime()).toBeInTheDocument();
95
123
  });
96
124
  it('should render time tooltip when type is time and interval is one hour', () => {
97
125
  const label = date;
@@ -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,19 +1,19 @@
1
+ import { TooltipContentProps } from 'recharts';
2
+ import { LegendShape } from '../chartlegend/ChartLegend';
3
+ import {
4
+ ChartTooltipPortal,
5
+ ChartTooltipHeader,
6
+ ChartTooltipItem,
7
+ ChartTooltipItemsContainer,
8
+ TooltipHeader,
9
+ } from '../charttooltip/ChartTooltip';
1
10
  import {
2
11
  BarchartBars,
3
12
  BarchartTooltipFn,
4
13
  CategoryType,
5
14
  TimeType,
6
15
  } from './Barchart.component';
7
- import { FormattedDateTime } from '../date/FormattedDateTime';
8
- import { TooltipContentProps } from 'recharts';
9
16
  import { getCurrentPoint } from './utils';
10
- import {
11
- ChartTooltipContainer,
12
- ChartTooltipItem,
13
- ChartTooltipHeader,
14
- ChartTooltipItemsContainer,
15
- } from '../charttooltip/ChartTooltip';
16
- import { LegendShape } from '../chartlegend/ChartLegend';
17
17
 
18
18
  export const BarchartTooltip = <T extends BarchartBars>({
19
19
  type,
@@ -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,30 +30,28 @@ 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
- }
43
42
 
44
- return (
45
- <ChartTooltipContainer>
43
+ const duration =
44
+ type.type === 'time'
45
+ ? type.timeRange.startDate.getTime() - type.timeRange.endDate.getTime()
46
+ : 0;
47
+
48
+ const tooltipContent = tooltip ? (
49
+ tooltip(currentPoint)
50
+ ) : (
51
+ <>
46
52
  <ChartTooltipHeader>
47
53
  {type.type === 'time' ? (
48
- <FormattedDateTime
49
- format={
50
- type.timeRange.interval < 24 * 60 * 60 * 1000
51
- ? 'day-month-abbreviated-hour-minute-second'
52
- : 'long-date-without-weekday'
53
- }
54
- value={new Date(currentPoint.category)}
55
- />
54
+ <TooltipHeader duration={duration} value={currentPoint.category} />
56
55
  ) : (
57
56
  currentPoint.category
58
57
  )}
@@ -84,6 +83,16 @@ export const BarchartTooltip = <T extends BarchartBars>({
84
83
  );
85
84
  })}
86
85
  </ChartTooltipItemsContainer>
87
- </ChartTooltipContainer>
86
+ </>
87
+ );
88
+
89
+ return (
90
+ <ChartTooltipPortal
91
+ coordinate={coordinate}
92
+ chartContainerRef={chartContainerRef}
93
+ isVisible={active}
94
+ >
95
+ {tooltipContent}
96
+ </ChartTooltipPortal>
88
97
  );
89
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
  ]);
@@ -1,30 +1,58 @@
1
- import {
2
- BarchartProps,
3
- BarchartBars,
4
- BarchartTooltipFn,
5
- } from './Barchart.component';
1
+ import { BarchartProps, BarchartBars } from './Barchart.component';
6
2
  import { TooltipContentProps } from 'recharts';
7
3
  import { chartColors, ChartColors } from '../../style/theme';
8
4
  import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
9
5
 
10
6
  export const getRoundReferenceValue = (value: number): number => {
11
- if (value <= 0) return 10; // Default for zero or negative values
7
+ if (value <= 0) return 1; // Default for zero or negative values
12
8
 
13
9
  // Get the magnitude (10^n where n is the number of digits - 1)
14
10
  const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
15
11
 
12
+ // Buffer the value by 10% to avoid being too close to the edge of the chart
13
+ const bufferedValue = value * 1.1;
14
+
16
15
  // Normalized value between 1 and 10
17
- const normalized = value / magnitude;
16
+ const normalized = bufferedValue / magnitude;
18
17
 
19
18
  // Round to nice numbers based on normalized value
19
+ // skip 1.5, 3, 4, 7.5 as top value for better chart
20
+ // appearance for small values
20
21
  let result: number;
22
+
21
23
  if (normalized <= 1) result = magnitude;
22
- else if (normalized <= 2.5) result = 2.5 * magnitude;
24
+ else if (normalized <= 2) result = 2 * magnitude;
25
+ else if (value > 10 && normalized <= 4) result = 4 * magnitude;
23
26
  else if (normalized <= 5) result = 5 * magnitude;
27
+ else if (value > 10 && normalized <= 7.5) result = 7.5 * magnitude;
24
28
  else result = 10 * magnitude;
25
29
 
26
- // Ensure minimum value of 5 for better chart appearance
27
- return Math.max(result, 5);
30
+ return result;
31
+ };
32
+
33
+ export const getTicks = (topValue: number, isSymmetrical: boolean) => {
34
+ if (topValue < 10) {
35
+ if (isSymmetrical) {
36
+ return [-topValue, 0, topValue];
37
+ } else {
38
+ return [0, topValue];
39
+ }
40
+ }
41
+ const numberOfTicks = topValue % 3 === 0 ? 4 : 3;
42
+ const tickInterval = topValue / (numberOfTicks - 1);
43
+ const ticks = Array.from(
44
+ { length: numberOfTicks },
45
+ (_, index) => index * tickInterval,
46
+ );
47
+ if (isSymmetrical) {
48
+ // Create negative ticks in order without 0
49
+ const negativeTicks = Array.from(
50
+ { length: numberOfTicks - 1 },
51
+ (_, index) => -(numberOfTicks - 1 - index) * tickInterval,
52
+ );
53
+ ticks.unshift(...negativeTicks);
54
+ }
55
+ return ticks;
28
56
  };
29
57
 
30
58
  export const getMaxBarValue = (
@@ -304,11 +332,11 @@ export const computeUnitLabelAndRoundReferenceValue = (
304
332
  ) => {
305
333
  if (!unitRange) {
306
334
  const roundReferenceValue = getRoundReferenceValue(maxValue);
307
- return { unitLabel: '', roundReferenceValue, rechartsData: data };
335
+ return { unitLabel: undefined, roundReferenceValue, rechartsData: data };
308
336
  }
309
337
 
310
338
  const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
311
- const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
339
+ const topValue = maxValue / valueBase;
312
340
  const roundReferenceValue = getRoundReferenceValue(topValue);
313
341
  const rechartsData = data.map((dataPoint) => {
314
342
  const normalizedDataPoint = { ...dataPoint };