@scality/core-ui 0.161.0 → 0.162.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 (136) hide show
  1. package/README.md +15 -15
  2. package/dist/components/accordion/Accordion.component.d.ts +0 -1
  3. package/dist/components/accordion/Accordion.component.d.ts.map +1 -1
  4. package/dist/components/barchartv2/Barchart.component.d.ts +55 -0
  5. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -0
  6. package/dist/components/barchartv2/Barchart.component.js +76 -0
  7. package/dist/components/barchartv2/utils.d.ts +95 -0
  8. package/dist/components/barchartv2/utils.d.ts.map +1 -0
  9. package/dist/components/barchartv2/utils.js +305 -0
  10. package/dist/components/buttonv2/Buttonv2.component.d.ts +1 -1
  11. package/dist/components/buttonv2/Buttonv2.component.d.ts.map +1 -1
  12. package/dist/components/constrainedtext/Constrainedtext.component.d.ts +2 -1
  13. package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
  14. package/dist/components/constrainedtext/Constrainedtext.component.js +5 -4
  15. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts +0 -1
  16. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
  17. package/dist/components/date/FormattedDateTime.d.ts +1 -0
  18. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  19. package/dist/components/date/FormattedDateTime.js +5 -0
  20. package/dist/components/emptytable/Emptytable.component.d.ts +0 -1
  21. package/dist/components/emptytable/Emptytable.component.d.ts.map +1 -1
  22. package/dist/components/emptytable/Emptytable.component.js +1 -0
  23. package/dist/components/error-pages/ErrorPage401.component.d.ts +0 -1
  24. package/dist/components/error-pages/ErrorPage401.component.d.ts.map +1 -1
  25. package/dist/components/error-pages/ErrorPage404.component.d.ts +0 -1
  26. package/dist/components/error-pages/ErrorPage404.component.d.ts.map +1 -1
  27. package/dist/components/error-pages/ErrorPage500.component.d.ts +0 -1
  28. package/dist/components/error-pages/ErrorPage500.component.d.ts.map +1 -1
  29. package/dist/components/error-pages/ErrorPageAuth.component.d.ts.map +1 -1
  30. package/dist/components/form/Form.component.d.ts +2 -2
  31. package/dist/components/form/Form.component.d.ts.map +1 -1
  32. package/dist/components/infomessage/InfoMessage.component.d.ts +0 -1
  33. package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
  34. package/dist/components/lateralnavbarlayout/LateralNavbarLayout.component.d.ts.map +1 -1
  35. package/dist/components/layout/Layout.component.d.ts.map +1 -1
  36. package/dist/components/layout/v2/panels.d.ts.map +1 -1
  37. package/dist/components/modal/Modal.component.js +2 -2
  38. package/dist/components/navbar/Navbar.component.js +2 -2
  39. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts +0 -1
  40. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts.map +1 -1
  41. package/dist/components/searchinput/SearchInput.component.d.ts +1 -2
  42. package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
  43. package/dist/components/selectv2/Selectv2.component.d.ts +5 -5
  44. package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
  45. package/dist/components/statuswrapper/Statuswrapper.component.d.ts +0 -1
  46. package/dist/components/statuswrapper/Statuswrapper.component.d.ts.map +1 -1
  47. package/dist/components/tablev2/Search.js +2 -2
  48. package/dist/components/tablev2/SingleSelectableContent.d.ts +1 -2
  49. package/dist/components/tablev2/SingleSelectableContent.d.ts.map +1 -1
  50. package/dist/components/tablev2/TableCommon.d.ts +2 -2
  51. package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
  52. package/dist/components/tablev2/TableSync.d.ts +8 -0
  53. package/dist/components/tablev2/TableSync.d.ts.map +1 -0
  54. package/dist/components/tablev2/TableSync.js +11 -0
  55. package/dist/components/tablev2/Tablev2.component.d.ts +2 -1
  56. package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
  57. package/dist/components/tablev2/Tablev2.component.js +10 -9
  58. package/dist/components/tabsv2/ScrollButton.d.ts +1 -2
  59. package/dist/components/tabsv2/ScrollButton.d.ts.map +1 -1
  60. package/dist/components/tabsv2/ScrollButton.js +2 -2
  61. package/dist/components/tabsv2/Tabsv2.component.d.ts +2 -2
  62. package/dist/components/tabsv2/Tabsv2.component.d.ts.map +1 -1
  63. package/dist/components/tabsv2/Tabsv2.component.js +2 -2
  64. package/dist/components/text/Text.component.d.ts +0 -1
  65. package/dist/components/text/Text.component.d.ts.map +1 -1
  66. package/dist/components/textarea/TextArea.component.d.ts +3 -3
  67. package/dist/components/textarea/TextArea.component.d.ts.map +1 -1
  68. package/dist/components/textbadge/TextBadge.component.d.ts +0 -1
  69. package/dist/components/textbadge/TextBadge.component.d.ts.map +1 -1
  70. package/dist/components/toast/Toast.component.d.ts +1 -1
  71. package/dist/components/toast/Toast.component.d.ts.map +1 -1
  72. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  73. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts +1 -2
  74. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
  75. package/dist/components/vegachartv2/VegaChartV2.component.d.ts +1 -2
  76. package/dist/components/vegachartv2/VegaChartV2.component.d.ts.map +1 -1
  77. package/dist/components/vegachartv2/VegaChartV2.component.js +2 -2
  78. package/dist/icons/branding.d.ts.map +1 -1
  79. package/dist/icons/scality-loading.d.ts.map +1 -1
  80. package/dist/next.d.ts +1 -0
  81. package/dist/next.d.ts.map +1 -1
  82. package/dist/next.js +1 -0
  83. package/dist/style/theme.d.ts +19 -0
  84. package/dist/style/theme.d.ts.map +1 -1
  85. package/dist/style/theme.js +18 -1
  86. package/package.json +6 -3
  87. package/setupTests.js +6 -0
  88. package/src/lib/components/accordion/Accordion.component.tsx +1 -1
  89. package/src/lib/components/barchartv2/Barchart.component.test.tsx +383 -0
  90. package/src/lib/components/barchartv2/Barchart.component.tsx +309 -0
  91. package/src/lib/components/barchartv2/utils.test.ts +782 -0
  92. package/src/lib/components/barchartv2/utils.ts +486 -0
  93. package/src/lib/components/buttonv2/Buttonv2.component.tsx +1 -1
  94. package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +22 -3
  95. package/src/lib/components/coreuithemeprovider/CoreUiThemeProvider.tsx +0 -1
  96. package/src/lib/components/date/FormattedDateTime.tsx +6 -0
  97. package/src/lib/components/emptytable/Emptytable.component.tsx +1 -1
  98. package/src/lib/components/error-pages/ErrorPage401.component.tsx +0 -1
  99. package/src/lib/components/error-pages/ErrorPage404.component.tsx +0 -1
  100. package/src/lib/components/error-pages/ErrorPage500.component.tsx +0 -1
  101. package/src/lib/components/error-pages/ErrorPageAuth.component.tsx +0 -1
  102. package/src/lib/components/form/Form.component.tsx +1 -1
  103. package/src/lib/components/infomessage/InfoMessage.component.tsx +0 -1
  104. package/src/lib/components/lateralnavbarlayout/LateralNavbarLayout.component.tsx +0 -1
  105. package/src/lib/components/layout/Layout.component.tsx +0 -1
  106. package/src/lib/components/layout/v2/panels.tsx +1 -1
  107. package/src/lib/components/modal/Modal.component.tsx +2 -2
  108. package/src/lib/components/navbar/Navbar.component.tsx +2 -2
  109. package/src/lib/components/scrollbarwrapper/ScrollbarWrapper.component.tsx +0 -1
  110. package/src/lib/components/searchinput/SearchInput.component.tsx +0 -1
  111. package/src/lib/components/selectv2/Selectv2.component.tsx +11 -9
  112. package/src/lib/components/sidebar/Sidebar.component.tsx +1 -1
  113. package/src/lib/components/statuswrapper/Statuswrapper.component.tsx +0 -1
  114. package/src/lib/components/tablev2/Search.tsx +2 -2
  115. package/src/lib/components/tablev2/SingleSelectableContent.tsx +2 -2
  116. package/src/lib/components/tablev2/TableCommon.tsx +1 -1
  117. package/src/lib/components/tablev2/TableSync.test.tsx +31 -0
  118. package/src/lib/components/tablev2/TableSync.tsx +36 -0
  119. package/src/lib/components/tablev2/Tablev2.component.tsx +11 -9
  120. package/src/lib/components/tabsv2/ScrollButton.tsx +2 -2
  121. package/src/lib/components/tabsv2/Tabsv2.component.tsx +6 -6
  122. package/src/lib/components/text/Text.component.tsx +4 -5
  123. package/src/lib/components/textarea/TextArea.component.tsx +3 -2
  124. package/src/lib/components/textbadge/TextBadge.component.tsx +0 -1
  125. package/src/lib/components/toast/Toast.component.tsx +1 -1
  126. package/src/lib/components/toast/ToastProvider.tsx +3 -1
  127. package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +1 -1
  128. package/src/lib/components/vegachartv2/VegaChartV2.component.tsx +2 -2
  129. package/src/lib/icons/branding.tsx +0 -2
  130. package/src/lib/icons/scality-loading.tsx +0 -2
  131. package/src/lib/next.ts +5 -0
  132. package/src/lib/style/theme.ts +24 -1
  133. package/stories/BarChart/barchart.stories.tsx +655 -0
  134. package/stories/areachart.stories.tsx +0 -1
  135. package/stories/tablev2.stories.tsx +41 -0
  136. package/tsconfig.json +5 -2
@@ -0,0 +1,383 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { getWrapper } from '../../testUtils';
4
+ import { Barchart } from './Barchart.component';
5
+
6
+ const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
7
+ const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
8
+
9
+ // Mock ResponsiveContainer to test the Barchart component
10
+ jest.mock('recharts', () => {
11
+ const OriginalResponsiveContainerModule = jest.requireActual('recharts');
12
+
13
+ return {
14
+ ...OriginalResponsiveContainerModule,
15
+ ResponsiveContainer: ({ height, children }) => (
16
+ <OriginalResponsiveContainerModule.ResponsiveContainer
17
+ width={800}
18
+ height={300}
19
+ data-testid="responsive-container"
20
+ >
21
+ {children}
22
+ </OriginalResponsiveContainerModule.ResponsiveContainer>
23
+ ),
24
+ };
25
+ });
26
+
27
+ const testBars = [
28
+ {
29
+ label: 'Success',
30
+ data: [
31
+ ['category1', 10],
32
+ ['category2', 20],
33
+ ['category3', 30],
34
+ ],
35
+ color: 'green',
36
+ },
37
+ ] as const;
38
+
39
+ const testTimeBars = [
40
+ {
41
+ label: 'Success',
42
+ data: [
43
+ [new Date('2024-07-05'), 10],
44
+ [new Date('2024-07-06'), 20],
45
+ [new Date('2024-07-07'), 30],
46
+ ],
47
+ color: 'green',
48
+ },
49
+ ] as const;
50
+
51
+ describe('Barchart', () => {
52
+ describe('Basic rendering', () => {
53
+ it('should render the Barchart component with category data', async () => {
54
+ const { Wrapper } = getWrapper();
55
+ render(
56
+ <Wrapper>
57
+ <Barchart
58
+ type="category"
59
+ bars={testBars}
60
+ colorSet={{
61
+ Success: 'lineColor1',
62
+ }}
63
+ />
64
+ </Wrapper>,
65
+ );
66
+
67
+ expect(screen.getByText('category1')).toBeInTheDocument();
68
+ expect(screen.getByText('category2')).toBeInTheDocument();
69
+ expect(screen.getByText('category3')).toBeInTheDocument();
70
+ });
71
+ it('should render the Barchart component with time data', async () => {
72
+ const { Wrapper } = getWrapper();
73
+ render(
74
+ <Wrapper>
75
+ <Barchart
76
+ type={{
77
+ type: 'time',
78
+ timeRange: {
79
+ startDate: new Date('2024-07-05'),
80
+ endDate: new Date('2024-07-07'),
81
+ interval: ONE_DAY_IN_MILLISECONDS,
82
+ },
83
+ }}
84
+ bars={testTimeBars}
85
+ colorSet={{
86
+ Success: 'lineColor1',
87
+ }}
88
+ />
89
+ </Wrapper>,
90
+ );
91
+
92
+ expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
93
+ expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
94
+ expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
95
+ });
96
+ });
97
+
98
+ describe('Time data', () => {
99
+ it('should render the chart with correct starting days even if the data is missing', async () => {
100
+ const { Wrapper } = getWrapper();
101
+ render(
102
+ <Wrapper>
103
+ <Barchart
104
+ type={{
105
+ type: 'time',
106
+ timeRange: {
107
+ startDate: new Date('2024-07-03'),
108
+ endDate: new Date('2024-07-07'),
109
+ interval: ONE_DAY_IN_MILLISECONDS,
110
+ },
111
+ }}
112
+ // data starts on 2024-07-05
113
+ bars={testTimeBars}
114
+ colorSet={{
115
+ Success: 'lineColor1',
116
+ }}
117
+ />
118
+ </Wrapper>,
119
+ );
120
+ expect(screen.getByText('Wed03Jul')).toBeInTheDocument();
121
+ expect(screen.getByText('Thu04Jul')).toBeInTheDocument();
122
+ expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
123
+ expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
124
+ expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
125
+ });
126
+ it('should render when there are missing data in the time range', async () => {
127
+ const bars = [
128
+ {
129
+ label: 'Success',
130
+ data: [
131
+ [new Date('2024-07-05'), 10], // Friday
132
+ [new Date('2024-07-08'), 15], // Monday
133
+ ] as [Date, number][],
134
+ color: 'green',
135
+ },
136
+ {
137
+ label: 'Failed',
138
+ data: [
139
+ [new Date('2024-07-05'), 2], // Friday
140
+ [new Date('2024-07-08'), 3], // Monday
141
+ ] as [Date, number][],
142
+ color: 'red',
143
+ },
144
+ ] as const;
145
+
146
+ const type = {
147
+ type: 'time' as const,
148
+ timeRange: {
149
+ startDate: new Date('2024-07-05'),
150
+ endDate: new Date('2024-07-08'),
151
+ interval: ONE_DAY_IN_MILLISECONDS,
152
+ },
153
+ };
154
+ const { Wrapper } = getWrapper();
155
+ render(
156
+ <Wrapper>
157
+ <Barchart
158
+ type={type}
159
+ bars={bars}
160
+ colorSet={{
161
+ Success: 'lineColor1',
162
+ Failed: 'lineColor2',
163
+ }}
164
+ />
165
+ </Wrapper>,
166
+ );
167
+
168
+ // Check that all days are present
169
+ await waitFor(() => {
170
+ expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
171
+ expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
172
+ expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
173
+ expect(screen.getByText('Mon08Jul')).toBeInTheDocument();
174
+ });
175
+ });
176
+ it('should render for a specific time range', async () => {
177
+ // 7 days data from 2024-07-05 to 2024-07-11
178
+ const testTimeBars = [
179
+ {
180
+ label: 'Success',
181
+ data: [
182
+ [new Date('2024-07-05'), 10],
183
+ [new Date('2024-07-06'), 10],
184
+ [new Date('2024-07-07'), 10],
185
+ [new Date('2024-07-08'), 10],
186
+ [new Date('2024-07-09'), 10],
187
+ [new Date('2024-07-10'), 10],
188
+ [new Date('2024-07-11'), 10],
189
+ ],
190
+ color: 'green',
191
+ },
192
+ ] as const;
193
+
194
+ const type = {
195
+ type: 'time' as const,
196
+ timeRange: {
197
+ startDate: new Date('2024-07-05'),
198
+ endDate: new Date('2024-07-11'),
199
+ interval: ONE_DAY_IN_MILLISECONDS,
200
+ },
201
+ };
202
+ const { Wrapper } = getWrapper();
203
+ render(
204
+ <Wrapper>
205
+ <Barchart
206
+ type={type}
207
+ bars={testTimeBars}
208
+ colorSet={{
209
+ Success: 'lineColor1',
210
+ }}
211
+ />
212
+ </Wrapper>,
213
+ );
214
+ await waitFor(() => {
215
+ expect(screen.getByText('Fri05Jul')).toBeInTheDocument();
216
+ expect(screen.getByText('Sat06Jul')).toBeInTheDocument();
217
+ expect(screen.getByText('Sun07Jul')).toBeInTheDocument();
218
+ expect(screen.getByText('Mon08Jul')).toBeInTheDocument();
219
+ expect(screen.getByText('Tue09Jul')).toBeInTheDocument();
220
+ expect(screen.getByText('Wed10Jul')).toBeInTheDocument();
221
+ expect(screen.getByText('Thu11Jul')).toBeInTheDocument();
222
+ });
223
+ });
224
+ it('should render the Barchart component with hourly intervals', async () => {
225
+ const testHourlyBars = [
226
+ {
227
+ label: 'Success',
228
+ data: [
229
+ [new Date('2024-07-05T10:00:00'), 10],
230
+ [new Date('2024-07-05T12:00:00'), 20],
231
+ ],
232
+ color: 'green',
233
+ },
234
+ ] as const;
235
+
236
+ const { Wrapper } = getWrapper();
237
+ render(
238
+ <Wrapper>
239
+ <Barchart
240
+ type={{
241
+ type: 'time',
242
+ timeRange: {
243
+ startDate: new Date('2024-07-05T10:00:00'),
244
+ endDate: new Date('2024-07-05T12:00:00'),
245
+ interval: ONE_HOUR_IN_MILLISECONDS,
246
+ },
247
+ }}
248
+ bars={testHourlyBars}
249
+ colorSet={{
250
+ Success: 'lineColor1',
251
+ }}
252
+ />
253
+ </Wrapper>,
254
+ );
255
+
256
+ expect(screen.getByText('10:00')).toBeInTheDocument();
257
+ expect(screen.getByText('11:00')).toBeInTheDocument();
258
+ expect(screen.getByText('12:00')).toBeInTheDocument();
259
+ });
260
+ });
261
+
262
+ it('should render stacked bars', () => {
263
+ const testStackedBars = [
264
+ {
265
+ label: 'Success',
266
+ data: [
267
+ ['category1', 10],
268
+ ['category2', 20],
269
+ ['category3', 30],
270
+ ],
271
+ color: 'green',
272
+ },
273
+ {
274
+ label: 'Failed',
275
+ data: [
276
+ ['category1', 5],
277
+ ['category2', 8],
278
+ ['category3', 12],
279
+ ],
280
+ color: 'red',
281
+ },
282
+ ] as const;
283
+
284
+ const { Wrapper } = getWrapper();
285
+ render(
286
+ <Wrapper>
287
+ <Barchart
288
+ type="category"
289
+ bars={testStackedBars}
290
+ stacked={true}
291
+ colorSet={{
292
+ Success: 'lineColor1',
293
+ Failed: 'lineColor2',
294
+ }}
295
+ />
296
+ </Wrapper>,
297
+ );
298
+
299
+ expect(screen.getByText('category1')).toBeInTheDocument();
300
+ expect(screen.getByText('category2')).toBeInTheDocument();
301
+ expect(screen.getByText('category3')).toBeInTheDocument();
302
+ });
303
+
304
+ it('should sort categories using defaultSort function', () => {
305
+ const testBars = [
306
+ {
307
+ label: 'Success',
308
+ data: [
309
+ ['category1', 10],
310
+ ['category2', 20],
311
+ ['category3', 30],
312
+ ],
313
+ color: 'green',
314
+ },
315
+ ] as const;
316
+
317
+ const { Wrapper } = getWrapper();
318
+ render(
319
+ <Wrapper>
320
+ <Barchart
321
+ type="category"
322
+ bars={testBars}
323
+ defaultSort={(pointA, pointB) => {
324
+ const valueA = pointA.Success;
325
+ const valueB = pointB.Success;
326
+ return valueB - valueA > 0 ? 1 : valueB - valueA < 0 ? -1 : 0;
327
+ }}
328
+ colorSet={{
329
+ Success: 'lineColor1',
330
+ }}
331
+ />
332
+ </Wrapper>,
333
+ );
334
+
335
+ // Categories should be rendered in descending order by value
336
+ const categories = screen.getAllByText(/category[123]/);
337
+ expect(categories[0]).toHaveTextContent('category3'); // 30 (highest)
338
+ expect(categories[1]).toHaveTextContent('category2'); // 20 (middle)
339
+ expect(categories[2]).toHaveTextContent('category1'); // 10 (lowest)
340
+ });
341
+
342
+ it('should render the Barchart component with loading state', () => {
343
+ const { Wrapper } = getWrapper();
344
+ render(
345
+ <Wrapper>
346
+ <Barchart
347
+ type="category"
348
+ bars={[]}
349
+ isLoading
350
+ colorSet={{
351
+ Success: 'lineColor1',
352
+ }}
353
+ />
354
+ </Wrapper>,
355
+ );
356
+ expect(screen.getByText('Loading Chart Data...')).toBeInTheDocument();
357
+ });
358
+ it('should render header with title, secondary title, right title and help tooltip', async () => {
359
+ const { Wrapper } = getWrapper();
360
+ render(
361
+ <Wrapper>
362
+ <Barchart
363
+ type="category"
364
+ bars={[]}
365
+ title="Test Title"
366
+ secondaryTitle="Test Secondary Title"
367
+ rightTitle="Test Right Title"
368
+ helpTooltip="Test Help Tooltip"
369
+ colorSet={{
370
+ Success: 'lineColor1',
371
+ }}
372
+ />
373
+ </Wrapper>,
374
+ );
375
+
376
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
377
+ expect(screen.getByText('Test Secondary Title')).toBeInTheDocument();
378
+ expect(screen.getByText('Test Right Title')).toBeInTheDocument();
379
+ await waitFor(() => {
380
+ expect(screen.getByLabelText('Test Help Tooltip')).toBeInTheDocument();
381
+ });
382
+ });
383
+ });
@@ -0,0 +1,309 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Bar,
4
+ BarChart,
5
+ CartesianGrid,
6
+ ReferenceLine,
7
+ ResponsiveContainer,
8
+ Tooltip,
9
+ TooltipContentProps,
10
+ XAxis,
11
+ YAxis,
12
+ } from 'recharts';
13
+ import styled, { useTheme } from 'styled-components';
14
+ import { spacing, Stack, Wrap } from '../../spacing';
15
+ import { chartColors, ChartColors, fontSize } from '../../style/theme';
16
+ import { Box } from '../box/Box';
17
+ import { ConstrainedText } from '../constrainedtext/Constrainedtext.component';
18
+ import { IconHelp } from '../iconhelper/IconHelper';
19
+ import { Loader } from '../loader/Loader.component';
20
+ import { Text } from '../text/Text.component';
21
+ import { renderTooltipContent, UnitRange, useChartData } from './utils';
22
+
23
+ const CHART_CONSTANTS = {
24
+ TICK_WIDTH_OFFSET: 5,
25
+ BAR_SIZE: 12,
26
+ MIN_POINT_SIZE: 1,
27
+ DEFAULT_HEIGHT: 200,
28
+ CHART_MARGIN: {
29
+ left: 0,
30
+ right: 0,
31
+ top: 0,
32
+ bottom: 0,
33
+ },
34
+ } as const;
35
+
36
+ /* ---------------------------------- TYPE ---------------------------------- */
37
+
38
+ export type TimeType = {
39
+ type: 'time';
40
+ timeRange: {
41
+ startDate: Date;
42
+ endDate: Date;
43
+ interval: number;
44
+ };
45
+ };
46
+ export type Point = {
47
+ key: string | number;
48
+ values: { label: string; value: number }[];
49
+ };
50
+
51
+ export type BarchartBars = readonly {
52
+ readonly label: string;
53
+ /**
54
+ * When using a time type, the data should be an array of [Date, value]
55
+ * so use Date instead of timestamp for transformation data in format fn
56
+ */
57
+ readonly data: readonly (readonly [string | Date, number | string])[];
58
+ }[];
59
+
60
+ export type BarchartTooltipFn<T extends BarchartBars> = (currentPoint: {
61
+ category: string | number;
62
+ values: { label: T[number]['label']; value: number; isHovered: boolean }[];
63
+ }) => React.ReactNode;
64
+
65
+ export type BarchartSortFn<T extends BarchartBars> = (
66
+ pointA: Record<T[number]['label'], number> & { category: string | number },
67
+ pointB: Record<T[number]['label'], number> & { category: string | number },
68
+ ) => 1 | -1 | 0;
69
+
70
+ export type BarchartProps<T extends BarchartBars> = {
71
+ type: 'category' | TimeType;
72
+ bars: T;
73
+ colorSet: Record<T[number]['label'], ChartColors | (string & {})>;
74
+ tooltip?: BarchartTooltipFn<T>;
75
+ defaultSort?: BarchartSortFn<T>;
76
+ unitRange?: UnitRange;
77
+ helpTooltip?: string;
78
+ stacked?: boolean;
79
+ title?: string;
80
+ secondaryTitle?: string;
81
+ rightTitle?: React.ReactNode;
82
+ height?: number;
83
+ isLoading?: boolean;
84
+ };
85
+
86
+ interface CustomTickProps {
87
+ x: number;
88
+ y: number;
89
+ payload: {
90
+ value: string | number;
91
+ };
92
+ visibleTicksCount: number;
93
+ width: number;
94
+ }
95
+
96
+ /* ---------------------------------- COMPONENTS ---------------------------------- */
97
+
98
+ const CustomTick = ({
99
+ x,
100
+ y,
101
+ payload,
102
+ visibleTicksCount,
103
+ width,
104
+ }: CustomTickProps) => {
105
+ const theme = useTheme();
106
+ const tickWidth =
107
+ width / visibleTicksCount - CHART_CONSTANTS.TICK_WIDTH_OFFSET;
108
+ const centerX = x - tickWidth / 2;
109
+
110
+ return (
111
+ <foreignObject
112
+ x={centerX}
113
+ y={y}
114
+ width={tickWidth}
115
+ color={theme.textSecondary}
116
+ overflow="visible"
117
+ >
118
+ <ConstrainedText
119
+ text={
120
+ <Text variant="Smaller" color="textSecondary">
121
+ {String(payload.value)}
122
+ </Text>
123
+ }
124
+ centered
125
+ tooltipStyle={{
126
+ backgroundColor: theme.backgroundLevel1,
127
+ padding: spacing.r10,
128
+ borderRadius: spacing.r8,
129
+ border: `1px solid ${theme.border}`,
130
+ position: 'absolute',
131
+ }}
132
+ />
133
+ </foreignObject>
134
+ );
135
+ };
136
+
137
+ const StyledResponsiveContainer = styled(ResponsiveContainer)`
138
+ // Avoid tooltip over constrained text to be cut off
139
+ & .recharts-surface {
140
+ overflow: visible;
141
+ }
142
+ `;
143
+
144
+ const ChartHeader = ({
145
+ title,
146
+ secondaryTitle,
147
+ helpTooltip,
148
+ rightTitle,
149
+ }: {
150
+ title?: string;
151
+ secondaryTitle?: string;
152
+ helpTooltip?: string;
153
+ rightTitle?: React.ReactNode;
154
+ }) => {
155
+ return (
156
+ <Wrap>
157
+ <Stack gap="r4">
158
+ <Text variant="ChartTitle">{title}</Text>
159
+ {helpTooltip && (
160
+ <IconHelp tooltipMessage={helpTooltip} title={helpTooltip} />
161
+ )}
162
+
163
+ {secondaryTitle && (
164
+ <Text
165
+ color="textSecondary"
166
+ style={{
167
+ marginLeft: spacing.r8,
168
+ }}
169
+ >
170
+ {secondaryTitle}
171
+ </Text>
172
+ )}
173
+ </Stack>
174
+
175
+ {rightTitle && <Text>{rightTitle}</Text>}
176
+ </Wrap>
177
+ );
178
+ };
179
+
180
+ const Loading = ({ height }: { height: number }) => {
181
+ return (
182
+ <Box
183
+ height={height}
184
+ style={{
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ display: 'flex',
188
+ }}
189
+ >
190
+ <Loader size="larger" children={<Text>Loading Chart Data...</Text>} />
191
+ </Box>
192
+ );
193
+ };
194
+
195
+ /* ---------------------------------- MAIN COMPONENT ---------------------------------- */
196
+
197
+ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
198
+ const theme = useTheme();
199
+ const [hoveredValue, setHoveredValue] = useState<string | undefined>();
200
+
201
+ const {
202
+ height = CHART_CONSTANTS.DEFAULT_HEIGHT,
203
+ bars,
204
+ type = 'category',
205
+ colorSet,
206
+ unitRange,
207
+ stacked,
208
+ defaultSort,
209
+ tooltip,
210
+ title,
211
+ secondaryTitle,
212
+ helpTooltip,
213
+ rightTitle,
214
+ isLoading,
215
+ } = props;
216
+
217
+ const { rechartsBars, unitLabel, roundReferenceValue, rechartsData } =
218
+ useChartData(bars, type, colorSet, stacked, defaultSort, unitRange);
219
+
220
+ return (
221
+ <Stack direction="vertical" gap="r8">
222
+ <ChartHeader
223
+ title={title}
224
+ secondaryTitle={secondaryTitle}
225
+ helpTooltip={helpTooltip}
226
+ rightTitle={rightTitle}
227
+ />
228
+ {isLoading ? (
229
+ <Loading height={height} />
230
+ ) : (
231
+ <StyledResponsiveContainer width="100%" height={height}>
232
+ <BarChart
233
+ data={rechartsData}
234
+ accessibilityLayer
235
+ barSize={CHART_CONSTANTS.BAR_SIZE}
236
+ height={height}
237
+ margin={CHART_CONSTANTS.CHART_MARGIN}
238
+ >
239
+ <CartesianGrid
240
+ vertical={false}
241
+ horizontal={false}
242
+ fill={theme.backgroundLevel1}
243
+ />
244
+ {rechartsBars.map((bar) => {
245
+ const { fill, dataKey, stackId } = bar;
246
+ return (
247
+ <Bar
248
+ key={dataKey}
249
+ dataKey={dataKey}
250
+ fill={chartColors[fill] || fill}
251
+ minPointSize={CHART_CONSTANTS.MIN_POINT_SIZE}
252
+ stackId={stackId}
253
+ onMouseOver={() => setHoveredValue(dataKey)}
254
+ onMouseLeave={() => setHoveredValue(undefined)}
255
+ />
256
+ );
257
+ })}
258
+
259
+ <YAxis
260
+ tickCount={1}
261
+ unit={` ${unitLabel}`}
262
+ domain={[0, roundReferenceValue]}
263
+ tickFormatter={
264
+ (value) =>
265
+ new Intl.NumberFormat('fr-FR').format(value.toFixed(0)) // Add a space as thousand separator
266
+ }
267
+ axisLine={false}
268
+ tick={{
269
+ fill: theme.textSecondary,
270
+ fontSize: fontSize.smaller,
271
+ }}
272
+ tickLine={false}
273
+ label={{
274
+ fill: theme.textSecondary,
275
+ }}
276
+ orientation="right"
277
+ />
278
+
279
+ <ReferenceLine
280
+ y={roundReferenceValue}
281
+ fill={theme.border}
282
+ strokeWidth={0.5} // Reduce stroke width to make it less visible
283
+ />
284
+ <XAxis
285
+ dataKey="category"
286
+ tick={(props) => <CustomTick {...props} />}
287
+ type="category"
288
+ interval={0}
289
+ allowDataOverflow={true}
290
+ tickLine={{
291
+ stroke: theme.textSecondary,
292
+ }}
293
+ axisLine={{
294
+ stroke: theme.textSecondary,
295
+ }}
296
+ />
297
+
298
+ <Tooltip
299
+ content={(props: TooltipContentProps<number, string>) =>
300
+ renderTooltipContent(props, tooltip, hoveredValue)
301
+ }
302
+ cursor={false}
303
+ />
304
+ </BarChart>
305
+ </StyledResponsiveContainer>
306
+ )}
307
+ </Stack>
308
+ );
309
+ };