@scality/core-ui 0.176.0 → 0.178.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 +2 -2
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +16 -10
  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 +85 -3
  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 +25 -16
  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 +136 -3
  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
@@ -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
  ]);
@@ -4,23 +4,55 @@ import { chartColors, ChartColors } from '../../style/theme';
4
4
  import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
5
5
 
6
6
  export const getRoundReferenceValue = (value: number): number => {
7
- if (value <= 0) return 10; // Default for zero or negative values
7
+ if (value <= 0) return 1; // Default for zero or negative values
8
8
 
9
9
  // Get the magnitude (10^n where n is the number of digits - 1)
10
10
  const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
11
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
+
12
15
  // Normalized value between 1 and 10
13
- const normalized = value / magnitude;
16
+ const normalized = bufferedValue / magnitude;
14
17
 
15
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
16
21
  let result: number;
22
+
17
23
  if (normalized <= 1) result = magnitude;
18
- 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;
19
26
  else if (normalized <= 5) result = 5 * magnitude;
27
+ else if (value > 10 && normalized <= 7.5) result = 7.5 * magnitude;
20
28
  else result = 10 * magnitude;
21
29
 
22
- // Ensure minimum value of 5 for better chart appearance
23
- 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;
24
56
  };
25
57
 
26
58
  export const getMaxBarValue = (
@@ -300,11 +332,11 @@ export const computeUnitLabelAndRoundReferenceValue = (
300
332
  ) => {
301
333
  if (!unitRange) {
302
334
  const roundReferenceValue = getRoundReferenceValue(maxValue);
303
- return { unitLabel: '', roundReferenceValue, rechartsData: data };
335
+ return { unitLabel: undefined, roundReferenceValue, rechartsData: data };
304
336
  }
305
337
 
306
338
  const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
307
- const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
339
+ const topValue = maxValue / valueBase;
308
340
  const roundReferenceValue = getRoundReferenceValue(topValue);
309
341
  const rechartsData = data.map((dataPoint) => {
310
342
  const normalizedDataPoint = { ...dataPoint };
@@ -1,4 +1,14 @@
1
1
  import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import {
5
+ useFloating,
6
+ autoUpdate,
7
+ offset,
8
+ flip,
9
+ shift,
10
+ Middleware,
11
+ } from '@floating-ui/react';
2
12
  import styled from 'styled-components';
3
13
  import { spacing } from '../../spacing';
4
14
  import { fontSize, fontWeight } from '../../style/theme';
@@ -12,7 +22,8 @@ export const ChartTooltipContainer = styled.div`
12
22
  font-size: ${fontSize.small};
13
23
  padding: ${spacing.r8};
14
24
  min-width: 10rem;
15
- max-width: 250px;
25
+ max-width: 40rem;
26
+
16
27
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
17
28
  `;
18
29
 
@@ -98,9 +109,9 @@ export type TooltipDateFormat =
98
109
  const getTooltipDateFormat: (duration: number) => TooltipDateFormat = (
99
110
  duration: number,
100
111
  ) => {
101
- if (duration <= 60 * 60 * 1000) {
112
+ if (duration <= 60 * 60) {
102
113
  return 'day-month-abbreviated-hour-minute-second';
103
- } else if (duration <= 7 * 24 * 60 * 60 * 1000) {
114
+ } else if (duration <= 7 * 24 * 60 * 60) {
104
115
  return 'day-month-abbreviated-hour-minute';
105
116
  } else {
106
117
  return 'day-month-abbreviated-year-hour-minute';
@@ -121,3 +132,125 @@ export const TooltipHeader = ({
121
132
  </ChartTooltipHeader>
122
133
  );
123
134
  };
135
+
136
+ export interface ChartTooltipPortalProps {
137
+ children: React.ReactNode;
138
+ coordinate?: { x: number; y: number };
139
+ chartContainerRef: React.RefObject<HTMLDivElement>;
140
+ isVisible?: boolean;
141
+ middleware?: Middleware[];
142
+ offset?: number | (({ placement }: { placement: string }) => number);
143
+ customPosition?: (
144
+ chartRect: DOMRect,
145
+ coordinate?: { x: number; y: number },
146
+ ) => { x: number; y: number };
147
+ containerComponent?: React.ComponentType<any>;
148
+ }
149
+
150
+ export const ChartTooltipPortal: React.FC<ChartTooltipPortalProps> = ({
151
+ children,
152
+ coordinate,
153
+ chartContainerRef,
154
+ isVisible = true,
155
+ middleware,
156
+ offset: customOffset,
157
+ customPosition,
158
+ containerComponent: ContainerComponent = ChartTooltipContainer,
159
+ }) => {
160
+ const [virtualElement, setVirtualElement] = useState<any>(null);
161
+ const previousPositionRef = useRef<{ x: number; y: number } | null>(null);
162
+ const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
163
+ null,
164
+ );
165
+
166
+ // Default middleware configuration
167
+ const defaultMiddleware = [
168
+ offset(customOffset || 20),
169
+ flip(),
170
+ shift({ padding: 10 }),
171
+ ];
172
+
173
+ const { refs, floatingStyles } = useFloating({
174
+ elements: {
175
+ reference: virtualElement,
176
+ },
177
+ placement: 'top',
178
+ middleware: middleware || defaultMiddleware,
179
+ whileElementsMounted: autoUpdate,
180
+ });
181
+
182
+ // Create portal container once
183
+ useEffect(() => {
184
+ const container = document.createElement('div');
185
+ document.body.appendChild(container);
186
+ setPortalContainer(container);
187
+
188
+ return () => {
189
+ document.body.removeChild(container);
190
+ };
191
+ }, []);
192
+
193
+ // Create virtual element from coordinate or custom position
194
+ useEffect(() => {
195
+ if (chartContainerRef.current) {
196
+ const chartRect = chartContainerRef.current.getBoundingClientRect();
197
+
198
+ let tooltipX: number;
199
+ let tooltipY: number;
200
+
201
+ if (customPosition) {
202
+ // Use custom positioning function
203
+ const position = customPosition(chartRect, coordinate);
204
+ tooltipX = position.x;
205
+ tooltipY = position.y;
206
+ } else if (coordinate) {
207
+ // Use default coordinate-based positioning
208
+ tooltipX = chartRect.left + coordinate.x;
209
+ tooltipY = chartRect.top + coordinate.y;
210
+ } else {
211
+ return; // No positioning method available
212
+ }
213
+
214
+ // Check if position has changed significantly
215
+ const hasPositionChanged =
216
+ !previousPositionRef.current ||
217
+ Math.abs(previousPositionRef.current.x - tooltipX) > 5 ||
218
+ Math.abs(previousPositionRef.current.y - tooltipY) > 5;
219
+
220
+ if (hasPositionChanged) {
221
+ previousPositionRef.current = { x: tooltipX, y: tooltipY };
222
+ }
223
+
224
+ setVirtualElement({
225
+ getBoundingClientRect() {
226
+ return {
227
+ width: 0,
228
+ height: 0,
229
+ x: tooltipX,
230
+ y: tooltipY,
231
+ left: tooltipX,
232
+ top: tooltipY,
233
+ right: tooltipX,
234
+ bottom: tooltipY,
235
+ };
236
+ },
237
+ });
238
+ }
239
+ }, [coordinate, chartContainerRef, customPosition]);
240
+
241
+ if (!isVisible || !virtualElement || !portalContainer) return null;
242
+
243
+ const tooltipContent = (
244
+ <ContainerComponent
245
+ ref={refs.setFloating}
246
+ style={{
247
+ ...floatingStyles,
248
+ opacity: isVisible ? 1 : 0,
249
+ }}
250
+ >
251
+ {children}
252
+ </ContainerComponent>
253
+ );
254
+
255
+ return createPortal(tooltipContent, portalContainer);
256
+ };
@@ -26,12 +26,17 @@ export interface GlobalHealthProps {
26
26
 
27
27
  const ChartInteractiveContainer = styled.div`
28
28
  position: relative;
29
+ outline: none;
30
+ .recharts-surface {
31
+ outline: none;
32
+ }
29
33
  `;
30
34
 
31
35
  export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
32
36
  const [tooltipData, setTooltipData] = useState<Alert | null>(null);
33
37
  const [focusedAlertIndex, setFocusedAlertIndex] = useState<number>(-1);
34
38
  const [keyboardActive, setKeyboardActive] = useState<boolean>(false);
39
+ const [activeBarKey, setActiveBarKey] = useState<string | null>(null);
35
40
  const chartContainerRef = useRef<HTMLDivElement>(null);
36
41
  const theme = useTheme();
37
42
  const startTimestamp = new Date(start).getTime();
@@ -47,6 +52,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
47
52
  const handlePointerEnter = useCallback(
48
53
  (key: string) => {
49
54
  setTooltipData(alertsMap[key]);
55
+ setActiveBarKey(key);
50
56
  },
51
57
  [alertsMap],
52
58
  );
@@ -54,6 +60,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
54
60
  const handlePointerLeave = useCallback(() => {
55
61
  if (!keyboardActive) {
56
62
  setTooltipData(null);
63
+ setActiveBarKey(null);
57
64
  }
58
65
  }, [keyboardActive]);
59
66
 
@@ -83,6 +90,11 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
83
90
  setFocusedAlertIndex(update.newIndex);
84
91
  setTooltipData(update.selectedAlert);
85
92
  setKeyboardActive(update.shouldActivateKeyboard);
93
+
94
+ // Set active bar key for keyboard navigation
95
+ if (update.selectedAlert) {
96
+ setActiveBarKey(update.selectedAlert.key);
97
+ }
86
98
  },
87
99
  [allAlertKeys, focusedAlertIndex],
88
100
  );
@@ -93,6 +105,9 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
93
105
  setFocusedAlertIndex(0);
94
106
  setTooltipData(allAlertKeys[0]);
95
107
  setKeyboardActive(true);
108
+
109
+ // Set active bar key for initial focus
110
+ setActiveBarKey(allAlertKeys[0].key);
96
111
  }
97
112
  }, [allAlertKeys, focusedAlertIndex]);
98
113
 
@@ -100,6 +115,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
100
115
  setKeyboardActive(false);
101
116
  setFocusedAlertIndex(-1);
102
117
  setTooltipData(null);
118
+ setActiveBarKey(null);
103
119
  }, []);
104
120
 
105
121
  // Handle mouse enter to disable keyboard mode
@@ -182,17 +198,46 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
182
198
  isAnimationActive={false}
183
199
  />
184
200
 
185
- {/* Alert bars */}
186
- {allAlertBars.map(({ key, fill }) => (
187
- <Bar
188
- dataKey={key}
189
- yAxisId={key}
190
- fill={fill}
191
- onPointerEnter={() => handlePointerEnter(key)}
192
- onPointerLeave={() => handlePointerLeave()}
193
- isAnimationActive={false}
194
- />
195
- ))}
201
+ {/* Alert bars - render non-active bars first */}
202
+ {allAlertBars.map(({ key, fill }) => {
203
+ const isActive = key === activeBarKey;
204
+ // Skip active bar here - it will be rendered last
205
+ if (isActive) return null;
206
+
207
+ return (
208
+ <Bar
209
+ key={key}
210
+ dataKey={key}
211
+ yAxisId={key}
212
+ fill={fill}
213
+ onPointerEnter={() => handlePointerEnter(key)}
214
+ onPointerLeave={() => handlePointerLeave()}
215
+ isAnimationActive={false}
216
+ />
217
+ );
218
+ })}
219
+
220
+ {/* Render active bar last to ensure it's on top */}
221
+ {activeBarKey &&
222
+ (() => {
223
+ const activeBar = allAlertBars.find(
224
+ (bar) => bar.key === activeBarKey,
225
+ );
226
+ if (!activeBar) return null;
227
+
228
+ return (
229
+ <Bar
230
+ key={`${activeBar.key}-active`}
231
+ dataKey={activeBar.key}
232
+ yAxisId={activeBar.key}
233
+ fill={activeBar.fill}
234
+ stroke={theme.selectedActive}
235
+ onPointerEnter={() => handlePointerEnter(activeBar.key)}
236
+ onPointerLeave={() => handlePointerLeave()}
237
+ isAnimationActive={false}
238
+ />
239
+ );
240
+ })()}
196
241
  </BarChart>
197
242
  </ResponsiveContainer>
198
243
  </ChartInteractiveContainer>
@@ -1,19 +1,11 @@
1
1
  import React from 'react';
2
- import { createPortal } from 'react-dom';
3
- import { useEffect, useState } from 'react';
4
2
  import styled, { css, useTheme } from 'styled-components';
5
- import {
6
- useFloating,
7
- autoUpdate,
8
- offset,
9
- flip,
10
- shift,
11
- } from '@floating-ui/react';
12
3
  import { FormattedDateTime, Stack, Text, Wrap, spacing } from '../../../index';
13
4
  import { Alert } from '../GlobalHealthBarRecharts.component';
14
5
  import { TooltipContentProps } from 'recharts';
15
6
  import { zIndex } from '../../../style/theme';
16
7
  import { CHART_CONFIG, getTooltipPosition } from '../healthBarUtils';
8
+ import { ChartTooltipPortal } from '../../charttooltip/ChartTooltip';
17
9
 
18
10
  interface GlobalHealthBarTooltipProps {
19
11
  tooltipData: Alert | null;
@@ -53,121 +45,87 @@ export const GlobalHealthBarTooltip = (props: GlobalHealthBarTooltipProps) => {
53
45
  endTimestamp = 0,
54
46
  } = props;
55
47
  const { coordinate } = tooltipProps;
56
- const [virtualElement, setVirtualElement] = useState<any>(null);
57
48
 
58
- const { refs, floatingStyles } = useFloating({
59
- elements: {
60
- reference: virtualElement,
61
- },
62
- middleware: [
63
- offset(({ placement }) => {
64
- // Use larger offset when tooltip is on top
65
- // to avoid tooltip over bar
66
- return placement.includes('top') ? 20 : 30;
67
- }),
68
- flip(),
69
- shift({ padding: 10 }),
70
- ],
71
- whileElementsMounted: autoUpdate,
72
- });
73
-
74
- // Create virtual element from coordinate
75
- useEffect(() => {
76
- if (chartContainerRef.current) {
77
- const chartRect = chartContainerRef.current.getBoundingClientRect();
49
+ if (!tooltipData) return null;
78
50
 
79
- let tooltipX: number;
80
- let tooltipY: number;
51
+ const { description, startsAt, endsAt, severity } = tooltipData;
81
52
 
82
- if (isKeyboardActive && tooltipData && startTimestamp && endTimestamp) {
83
- // Calculate the chart's usable width (excluding margins)
84
- const chartUsableWidth =
85
- chartRect.width -
86
- CHART_CONFIG.MARGINS.left -
87
- CHART_CONFIG.MARGINS.right;
53
+ const tooltipContent = (
54
+ <Stack direction="vertical" gap="r8">
55
+ <Wrap>
56
+ <Text variant="Smaller">Severity</Text>
57
+ <Text color="textPrimary" variant="Smaller">
58
+ {severity}
59
+ </Text>
60
+ </Wrap>
61
+ <Wrap>
62
+ <Text variant="Smaller">Start</Text>
63
+ <Text color="textPrimary" variant="Smaller">
64
+ <FormattedDateTime format="date-time" value={new Date(startsAt)} />
65
+ </Text>
66
+ </Wrap>
67
+ <Wrap>
68
+ <Text variant="Smaller">End</Text>
69
+ <Text color="textPrimary" variant="Smaller">
70
+ <FormattedDateTime format="date-time" value={new Date(endsAt)} />
71
+ </Text>
72
+ </Wrap>
73
+ <Wrap>
74
+ <Text variant="Smaller" style={{ paddingRight: spacing.r32 }}>
75
+ Description
76
+ </Text>
77
+ <Text
78
+ color="textPrimary"
79
+ variant="Smaller"
80
+ style={{ whiteSpace: 'wrap', textAlign: 'justify' }}
81
+ >
82
+ {description}
83
+ </Text>
84
+ </Wrap>
85
+ </Stack>
86
+ );
88
87
 
89
- // Use the same positioning logic as alert bars
90
- const alertCenterX = getTooltipPosition(
91
- tooltipData,
92
- startTimestamp,
93
- endTimestamp,
94
- chartUsableWidth,
95
- );
88
+ return (
89
+ <ChartTooltipPortal
90
+ coordinate={coordinate}
91
+ chartContainerRef={chartContainerRef}
92
+ isVisible={!!tooltipData}
93
+ customPosition={(chartRect, coordinate) => {
94
+ if (isKeyboardActive && tooltipData && startTimestamp && endTimestamp) {
95
+ // Calculate the chart's usable width (excluding margins)
96
+ const chartUsableWidth =
97
+ chartRect.width -
98
+ CHART_CONFIG.MARGINS.left -
99
+ CHART_CONFIG.MARGINS.right;
96
100
 
97
- // Position tooltip at the center of the alert's time span
98
- // alertCenterX already includes the margin offset, so just add chartRect.left
99
- tooltipX = chartRect.left + alertCenterX;
100
- tooltipY = chartRect.top + CHART_CONFIG.BAR_SIZE;
101
- } else {
102
- // For mouse navigation, use the provided coordinate
103
- tooltipX = chartRect.left + coordinate?.x;
104
- tooltipY = chartRect.top + coordinate?.y;
105
- }
101
+ // Use the same positioning logic as alert bars
102
+ const alertCenterX = getTooltipPosition(
103
+ tooltipData,
104
+ startTimestamp,
105
+ endTimestamp,
106
+ chartUsableWidth,
107
+ );
106
108
 
107
- setVirtualElement({
108
- getBoundingClientRect() {
109
109
  return {
110
- width: 0,
111
- height: 0,
112
- x: tooltipX,
113
- y: tooltipY,
114
- left: tooltipX,
115
- top: tooltipY,
116
- right: tooltipX,
117
- bottom: tooltipY,
110
+ x: chartRect.left + alertCenterX,
111
+ y: chartRect.top + CHART_CONFIG.BAR_SIZE,
118
112
  };
119
- },
120
- });
121
- }
122
- }, [
123
- coordinate,
124
- chartContainerRef,
125
- isKeyboardActive,
126
- tooltipData,
127
- startTimestamp,
128
- endTimestamp,
129
- ]);
130
-
131
- if (!tooltipData) return null;
132
-
133
- const { description, startsAt, endsAt, severity } = tooltipData;
134
-
135
- const tooltipContent = (
136
- <TooltipContainer ref={refs.setFloating} style={floatingStyles}>
137
- <Stack direction="vertical" gap="r8">
138
- <Wrap>
139
- <Text variant="Smaller">Severity</Text>
140
- <Text color="textPrimary" variant="Smaller">
141
- {severity}
142
- </Text>
143
- </Wrap>
144
- <Wrap>
145
- <Text variant="Smaller">Start</Text>
146
- <Text color="textPrimary" variant="Smaller">
147
- <FormattedDateTime format="date-time" value={new Date(startsAt)} />
148
- </Text>
149
- </Wrap>
150
- <Wrap>
151
- <Text variant="Smaller">End</Text>
152
- <Text color="textPrimary" variant="Smaller">
153
- <FormattedDateTime format="date-time" value={new Date(endsAt)} />
154
- </Text>
155
- </Wrap>
156
- <Wrap>
157
- <Text variant="Smaller" style={{ paddingRight: spacing.r32 }}>
158
- Description
159
- </Text>
160
- <Text
161
- color="textPrimary"
162
- variant="Smaller"
163
- style={{ whiteSpace: 'wrap', textAlign: 'justify' }}
164
- >
165
- {description}
166
- </Text>
167
- </Wrap>
168
- </Stack>
169
- </TooltipContainer>
113
+ } else {
114
+ // For mouse navigation, use the provided coordinate
115
+ return {
116
+ x: chartRect.left + (coordinate?.x || 0),
117
+ y: chartRect.top + (coordinate?.y || 0),
118
+ };
119
+ }
120
+ }}
121
+ containerComponent={TooltipContainer}
122
+ offset={({ placement }) => {
123
+ // Use larger offset when tooltip is on top
124
+ // to avoid tooltip over bar
125
+ return placement.includes('top') ? 20 : 30;
126
+ }}
127
+ >
128
+ {tooltipContent}
129
+ </ChartTooltipPortal>
170
130
  );
171
-
172
- return createPortal(tooltipContent, document.body);
173
131
  };