@scality/core-ui 0.161.0 → 0.163.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 (173) 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 +53 -0
  5. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -0
  6. package/dist/components/barchartv2/Barchart.component.js +86 -0
  7. package/dist/components/barchartv2/utils.d.ts +118 -0
  8. package/dist/components/barchartv2/utils.d.ts.map +1 -0
  9. package/dist/components/barchartv2/utils.js +337 -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/chartlegend/ChartLegend.d.ts +8 -0
  13. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -0
  14. package/dist/components/chartlegend/ChartLegend.js +65 -0
  15. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +17 -0
  16. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -0
  17. package/dist/components/chartlegend/ChartLegendWrapper.js +50 -0
  18. package/dist/components/constrainedtext/Constrainedtext.component.d.ts +2 -1
  19. package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
  20. package/dist/components/constrainedtext/Constrainedtext.component.js +5 -4
  21. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts +0 -1
  22. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
  23. package/dist/components/date/FormattedDateTime.d.ts +4 -1
  24. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  25. package/dist/components/date/FormattedDateTime.js +24 -1
  26. package/dist/components/date/FormattedDateTime.spec.js +12 -0
  27. package/dist/components/emptytable/Emptytable.component.d.ts +0 -1
  28. package/dist/components/emptytable/Emptytable.component.d.ts.map +1 -1
  29. package/dist/components/emptytable/Emptytable.component.js +1 -0
  30. package/dist/components/error-pages/ErrorPage401.component.d.ts +0 -1
  31. package/dist/components/error-pages/ErrorPage401.component.d.ts.map +1 -1
  32. package/dist/components/error-pages/ErrorPage404.component.d.ts +0 -1
  33. package/dist/components/error-pages/ErrorPage404.component.d.ts.map +1 -1
  34. package/dist/components/error-pages/ErrorPage500.component.d.ts +0 -1
  35. package/dist/components/error-pages/ErrorPage500.component.d.ts.map +1 -1
  36. package/dist/components/error-pages/ErrorPageAuth.component.d.ts.map +1 -1
  37. package/dist/components/form/Form.component.d.ts +2 -2
  38. package/dist/components/form/Form.component.d.ts.map +1 -1
  39. package/dist/components/icon/Icon.component.d.ts +5 -5
  40. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  41. package/dist/components/icon/Icon.component.js +33 -31
  42. package/dist/components/infomessage/InfoMessage.component.d.ts +0 -1
  43. package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
  44. package/dist/components/lateralnavbarlayout/LateralNavbarLayout.component.d.ts.map +1 -1
  45. package/dist/components/layout/Layout.component.d.ts.map +1 -1
  46. package/dist/components/layout/v2/panels.d.ts.map +1 -1
  47. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +33 -0
  48. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -0
  49. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +249 -0
  50. package/dist/components/modal/Modal.component.js +2 -2
  51. package/dist/components/navbar/Navbar.component.js +2 -2
  52. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts +0 -1
  53. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts.map +1 -1
  54. package/dist/components/searchinput/SearchInput.component.d.ts +1 -2
  55. package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
  56. package/dist/components/selectv2/Selectv2.component.d.ts +5 -5
  57. package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
  58. package/dist/components/selectv2/Selectv2.component.js +11 -6
  59. package/dist/components/statuswrapper/Statuswrapper.component.d.ts +0 -1
  60. package/dist/components/statuswrapper/Statuswrapper.component.d.ts.map +1 -1
  61. package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
  62. package/dist/components/steppers/Stepper.component.js +9 -8
  63. package/dist/components/tablev2/Search.js +2 -2
  64. package/dist/components/tablev2/SingleSelectableContent.d.ts +1 -2
  65. package/dist/components/tablev2/SingleSelectableContent.d.ts.map +1 -1
  66. package/dist/components/tablev2/TableCommon.d.ts +2 -2
  67. package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
  68. package/dist/components/tablev2/TableSync.d.ts +8 -0
  69. package/dist/components/tablev2/TableSync.d.ts.map +1 -0
  70. package/dist/components/tablev2/TableSync.js +11 -0
  71. package/dist/components/tablev2/Tablev2.component.d.ts +2 -1
  72. package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
  73. package/dist/components/tablev2/Tablev2.component.js +10 -9
  74. package/dist/components/tabsv2/ScrollButton.d.ts +1 -2
  75. package/dist/components/tabsv2/ScrollButton.d.ts.map +1 -1
  76. package/dist/components/tabsv2/ScrollButton.js +2 -2
  77. package/dist/components/tabsv2/Tabsv2.component.d.ts +2 -2
  78. package/dist/components/tabsv2/Tabsv2.component.d.ts.map +1 -1
  79. package/dist/components/tabsv2/Tabsv2.component.js +2 -2
  80. package/dist/components/text/Text.component.d.ts +0 -1
  81. package/dist/components/text/Text.component.d.ts.map +1 -1
  82. package/dist/components/textarea/TextArea.component.d.ts +3 -3
  83. package/dist/components/textarea/TextArea.component.d.ts.map +1 -1
  84. package/dist/components/textbadge/TextBadge.component.d.ts +0 -1
  85. package/dist/components/textbadge/TextBadge.component.d.ts.map +1 -1
  86. package/dist/components/toast/Toast.component.d.ts +1 -1
  87. package/dist/components/toast/Toast.component.d.ts.map +1 -1
  88. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  89. package/dist/components/toast/ToastProvider.js +4 -5
  90. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts +1 -2
  91. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
  92. package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
  93. package/dist/components/vegachartv2/VegaChartV2.component.d.ts +1 -2
  94. package/dist/components/vegachartv2/VegaChartV2.component.d.ts.map +1 -1
  95. package/dist/components/vegachartv2/VegaChartV2.component.js +2 -2
  96. package/dist/icons/branding.d.ts.map +1 -1
  97. package/dist/icons/scality-loading.d.ts.map +1 -1
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1 -0
  101. package/dist/next.d.ts +2 -0
  102. package/dist/next.d.ts.map +1 -1
  103. package/dist/next.js +2 -0
  104. package/dist/style/theme.d.ts +20 -0
  105. package/dist/style/theme.d.ts.map +1 -1
  106. package/dist/style/theme.js +46 -1
  107. package/package.json +7 -4
  108. package/setupTests.js +6 -0
  109. package/src/lib/components/accordion/Accordion.component.tsx +1 -1
  110. package/src/lib/components/accordion/Accordion.test.tsx +7 -15
  111. package/src/lib/components/barchartv2/Barchart.component.test.tsx +364 -0
  112. package/src/lib/components/barchartv2/Barchart.component.tsx +321 -0
  113. package/src/lib/components/barchartv2/utils.test.ts +899 -0
  114. package/src/lib/components/barchartv2/utils.ts +534 -0
  115. package/src/lib/components/buttonv2/Buttonv2.component.tsx +1 -1
  116. package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
  117. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
  118. package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +22 -3
  119. package/src/lib/components/coreuithemeprovider/CoreUiThemeProvider.tsx +0 -1
  120. package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
  121. package/src/lib/components/date/FormattedDateTime.tsx +42 -2
  122. package/src/lib/components/emptytable/Emptytable.component.tsx +1 -1
  123. package/src/lib/components/error-pages/ErrorPage401.component.tsx +0 -1
  124. package/src/lib/components/error-pages/ErrorPage404.component.tsx +0 -1
  125. package/src/lib/components/error-pages/ErrorPage500.component.tsx +0 -1
  126. package/src/lib/components/error-pages/ErrorPageAuth.component.tsx +0 -1
  127. package/src/lib/components/form/Form.component.tsx +1 -1
  128. package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +3 -3
  129. package/src/lib/components/icon/Icon.component.tsx +48 -60
  130. package/src/lib/components/infomessage/InfoMessage.component.tsx +0 -1
  131. package/src/lib/components/inlineinput/InlineInput.test.tsx +22 -19
  132. package/src/lib/components/inputlist/InputList.test.tsx +21 -19
  133. package/src/lib/components/lateralnavbarlayout/LateralNavbarLayout.component.tsx +0 -1
  134. package/src/lib/components/layout/Layout.component.tsx +0 -1
  135. package/src/lib/components/layout/v2/panels.tsx +1 -1
  136. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
  137. package/src/lib/components/modal/Modal.component.tsx +2 -2
  138. package/src/lib/components/navbar/Navbar.component.tsx +2 -2
  139. package/src/lib/components/scrollbarwrapper/ScrollbarWrapper.component.tsx +0 -1
  140. package/src/lib/components/searchinput/SearchInput.component.tsx +0 -1
  141. package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
  142. package/src/lib/components/selectv2/Selectv2.component.tsx +24 -14
  143. package/src/lib/components/selectv2/selectv2.test.tsx +62 -57
  144. package/src/lib/components/sidebar/Sidebar.component.tsx +1 -1
  145. package/src/lib/components/statuswrapper/Statuswrapper.component.tsx +0 -1
  146. package/src/lib/components/steppers/Stepper.component.tsx +10 -8
  147. package/src/lib/components/tablev2/Search.tsx +2 -2
  148. package/src/lib/components/tablev2/SingleSelectableContent.tsx +2 -2
  149. package/src/lib/components/tablev2/TableCommon.tsx +1 -1
  150. package/src/lib/components/tablev2/TableSync.test.tsx +28 -0
  151. package/src/lib/components/tablev2/TableSync.tsx +36 -0
  152. package/src/lib/components/tablev2/Tablev2.component.tsx +11 -9
  153. package/src/lib/components/tablev2/Tablev2.test.tsx +36 -37
  154. package/src/lib/components/tabsv2/ScrollButton.tsx +2 -2
  155. package/src/lib/components/tabsv2/Tabsv2.component.tsx +6 -6
  156. package/src/lib/components/text/Text.component.tsx +4 -5
  157. package/src/lib/components/textarea/TextArea.component.tsx +3 -2
  158. package/src/lib/components/textbadge/TextBadge.component.tsx +0 -1
  159. package/src/lib/components/toast/Toast.component.tsx +1 -1
  160. package/src/lib/components/toast/ToastProvider.tsx +17 -7
  161. package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
  162. package/src/lib/components/vegachartv2/VegaChartV2.component.tsx +2 -2
  163. package/src/lib/icons/branding.tsx +0 -2
  164. package/src/lib/icons/scality-loading.tsx +0 -2
  165. package/src/lib/index.ts +1 -0
  166. package/src/lib/next.ts +6 -0
  167. package/src/lib/style/theme.ts +53 -1
  168. package/stories/BarChart/barchart.stories.tsx +822 -0
  169. package/stories/areachart.stories.tsx +0 -1
  170. package/stories/format.mdx +4 -2
  171. package/stories/linetimeseriechart.stories.tsx +485 -0
  172. package/stories/tablev2.stories.tsx +41 -0
  173. package/tsconfig.json +5 -2
@@ -0,0 +1,534 @@
1
+ import {
2
+ BarchartProps,
3
+ BarchartBars,
4
+ BarchartTooltipFn,
5
+ } from './Barchart.component';
6
+ import { DAY_MONTH_FORMATER, TIME_FORMATER } from '../date/FormattedDateTime';
7
+ import { TooltipContentProps } from 'recharts';
8
+ import { chartColors, ChartColors } from '../../style/theme';
9
+ import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
10
+
11
+ export const getRoundReferenceValue = (value: number): number => {
12
+ if (value <= 0) return 10; // Default for zero or negative values
13
+
14
+ // Get the magnitude (10^n where n is the number of digits - 1)
15
+ const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
16
+
17
+ // Normalized value between 1 and 10
18
+ const normalized = value / magnitude;
19
+
20
+ // Round to nice numbers based on normalized value
21
+ if (normalized <= 1) return magnitude;
22
+ if (normalized <= 2.5) return 2.5 * magnitude;
23
+ if (normalized <= 5) return 5 * magnitude;
24
+ return 10 * magnitude;
25
+ };
26
+
27
+ export const getMaxBarValue = (
28
+ data: { [key: string]: string | number }[],
29
+ stacked?: boolean,
30
+ ) => {
31
+ const values = data.map((item) => {
32
+ // If stacked, we need to filter out category and sum the values in the same object
33
+ if (stacked) {
34
+ // Get objects keys except category
35
+ const filterOutCategory = Object.keys(item).filter(
36
+ (key) => key !== 'category',
37
+ );
38
+ // Sum the values in the same object (corresponding to one bar) based on the keys
39
+ const sumValues = filterOutCategory.reduce((acc, curr) => {
40
+ return acc + Number(item[curr]);
41
+ }, 0);
42
+ return sumValues;
43
+ }
44
+ //filter out the category key
45
+ const numberValues = Object.keys(item)
46
+ .filter((key) => key !== 'category')
47
+ .map((key) => Number(item[key]));
48
+ // Get the max value among the values in the object (corresponding to one bar)
49
+ return Math.max(...numberValues);
50
+ });
51
+ return Math.max(...values);
52
+ };
53
+
54
+ /**
55
+ * Generates time ranges between start and end dates based on the given interval
56
+ * @param startDate - Start date
57
+ * @param endDate - End date
58
+ * @param interval - Interval in milliseconds
59
+ * @returns Array of time ranges with start and end properties as Date objects
60
+ */
61
+ const generateTimeRanges = (
62
+ startDate: Date,
63
+ endDate: Date,
64
+ interval: number,
65
+ ): { start: Date; end: Date }[] => {
66
+ const ranges: { start: Date; end: Date }[] = [];
67
+ if (!startDate || !endDate || !interval) {
68
+ return ranges;
69
+ }
70
+
71
+ let currentDate = new Date(startDate.getTime());
72
+ while (currentDate.getTime() <= endDate.getTime()) {
73
+ const rangeEnd = new Date(currentDate.getTime() + interval);
74
+
75
+ ranges.push({
76
+ start: new Date(currentDate.getTime()),
77
+ end: rangeEnd,
78
+ });
79
+
80
+ currentDate = new Date(currentDate.getTime() + interval);
81
+ }
82
+
83
+ return ranges;
84
+ };
85
+
86
+ /**
87
+ * Formats a date based on the interval
88
+ * @param date - Date object
89
+ * @param interval - Interval in milliseconds
90
+ * @returns Formatted string
91
+ */
92
+ const formatDate = (date: Date, interval: number): string => {
93
+ if (interval > 24 * 60 * 60 * 1000) {
94
+ return (
95
+ DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '') +
96
+ ' ' +
97
+ TIME_FORMATER.format(date)
98
+ );
99
+ } else if (interval === 24 * 60 * 60 * 1000) {
100
+ // Daily or longer intervals - use day format
101
+ return DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '');
102
+ } else if (interval >= 60 * 1000) {
103
+ //Handle hourly and minute intervals - use minute format
104
+ return TIME_FORMATER.format(date);
105
+ } else {
106
+ // Second intervals or less - use full timestamp
107
+ return date.toISOString();
108
+ }
109
+ };
110
+
111
+ /**
112
+ * Finds the time range that contains the given date
113
+ * @param date - Data point date
114
+ * @param ranges - Array of time ranges
115
+ * @returns The range that contains the date, or null if not found
116
+ */
117
+ const findRangeForDate = (
118
+ date: Date,
119
+ ranges: { start: Date; end: Date }[],
120
+ ): { start: Date; end: Date } | null => {
121
+ const timestamp = date.getTime();
122
+ return (
123
+ ranges.find(
124
+ (range) =>
125
+ timestamp >= range.start.getTime() && timestamp < range.end.getTime(),
126
+ ) || null
127
+ );
128
+ };
129
+
130
+ /**
131
+ * Transforms time-based data into chart format
132
+ */
133
+ export const transformTimeData = <T extends BarchartBars>(
134
+ bars: T,
135
+ type: {
136
+ type: 'time';
137
+ timeRange: {
138
+ startDate: Date;
139
+ endDate: Date;
140
+ interval: number;
141
+ };
142
+ },
143
+ barDataKeys: string[],
144
+ ) => {
145
+ const timeRanges = generateTimeRanges(
146
+ type.timeRange.startDate,
147
+ type.timeRange.endDate,
148
+ type.timeRange.interval,
149
+ );
150
+
151
+ const categoryMap = new Map<
152
+ string | number,
153
+ { [key: string]: string | number }
154
+ >();
155
+
156
+ // Initialize all ranges with zeros
157
+ timeRanges.forEach((range) => {
158
+ const categoryDisplay = formatDate(range.start, type.timeRange.interval);
159
+ const initialData: { [key: string]: string | number } = {
160
+ category: categoryDisplay,
161
+ };
162
+ barDataKeys.forEach((dataKey) => {
163
+ initialData[dataKey] = 0;
164
+ });
165
+ categoryMap.set(range.start.getTime(), initialData);
166
+ });
167
+
168
+ // Populate with actual data
169
+ bars.forEach((bar) => {
170
+ bar.data.forEach(([dateValue, value]) => {
171
+ // Convert to Date if it's not already a Date object
172
+ const date =
173
+ dateValue instanceof Date
174
+ ? dateValue
175
+ : new Date(dateValue as string | number);
176
+ const range = findRangeForDate(date, timeRanges);
177
+ if (range) {
178
+ const existingData = categoryMap.get(range.start.getTime())!;
179
+ existingData[bar.label] = value;
180
+ }
181
+ });
182
+ });
183
+
184
+ return Array.from(categoryMap.values());
185
+ };
186
+
187
+ /**
188
+ * Transforms category-based data into chart format
189
+ */
190
+ export const transformCategoryData = <T extends BarchartBars>(
191
+ bars: T,
192
+ barDataKeys: string[],
193
+ ) => {
194
+ const categoryMap = new Map<
195
+ string | number,
196
+ { [key: string]: string | number }
197
+ >();
198
+
199
+ bars.forEach((bar) => {
200
+ bar.data.forEach(([key, value]) => {
201
+ const categoryKey = String(key);
202
+
203
+ if (!categoryMap.has(categoryKey)) {
204
+ const newData: { [key: string]: string | number } = {
205
+ category: categoryKey,
206
+ };
207
+ barDataKeys.forEach((dataKey) => {
208
+ newData[dataKey] = 0;
209
+ });
210
+ categoryMap.set(categoryKey, newData);
211
+ }
212
+
213
+ const existingData = categoryMap.get(categoryKey)!;
214
+ existingData[bar.label] = value;
215
+ });
216
+ });
217
+
218
+ return Array.from(categoryMap.values());
219
+ };
220
+
221
+ /**
222
+ * Applies custom sorting to chart data
223
+ */
224
+ export const applySortingToData = <T extends BarchartBars>(
225
+ data: { [key: string]: string | number }[],
226
+ barDataKeys: string[],
227
+ defaultSort: BarchartProps<T>['defaultSort'],
228
+ ) => {
229
+ const points = data.map((item) => {
230
+ const point: any = { category: item.category };
231
+ barDataKeys.forEach((dataKey) => {
232
+ point[dataKey] = Number(item[dataKey]) || 0;
233
+ });
234
+ return point;
235
+ });
236
+
237
+ points.sort(defaultSort);
238
+
239
+ return points.map((point) => {
240
+ const dataItem: { [key: string]: string | number } = {
241
+ category: point.category,
242
+ };
243
+ barDataKeys.forEach((dataKey) => {
244
+ dataItem[dataKey] = point[dataKey];
245
+ });
246
+ return dataItem;
247
+ });
248
+ };
249
+
250
+ const getRechartsBarsAndBarDataKeys = (
251
+ bars: BarchartBars,
252
+ colorSet: Record<string, ChartColors | string>,
253
+ stacked?: boolean,
254
+ ) => {
255
+ const rechartsBars: { dataKey: string; fill: string; stackId?: string }[] =
256
+ [];
257
+ const barDataKeys: string[] = [];
258
+
259
+ bars.forEach((bar) => {
260
+ const colorName = colorSet[bar.label];
261
+ const rechartsBar = {
262
+ dataKey: bar.label,
263
+ fill: chartColors[colorName] || colorName,
264
+ stackId: stacked ? 'stacked' : undefined,
265
+ };
266
+
267
+ rechartsBars.push(rechartsBar);
268
+ barDataKeys.push(bar.label);
269
+ });
270
+
271
+ return {
272
+ rechartsBars,
273
+ barDataKeys,
274
+ };
275
+ };
276
+
277
+ /**
278
+ * Converts prometheus data to recharts data format
279
+ * @param bars - The bars to convert
280
+ * @param type - The chart type (category or time)
281
+ * @returns Recharts data format
282
+ */
283
+ export const formatPrometheusDataToRechartsDataAndBars = <
284
+ T extends BarchartBars,
285
+ >(
286
+ bars: T,
287
+ type: BarchartProps<T>['type'],
288
+ colorSet: Record<string, ChartColors | string>,
289
+ stacked?: boolean,
290
+ defaultSort?: BarchartProps<T>['defaultSort'],
291
+ ): {
292
+ data: { [key: string]: string | number }[];
293
+ rechartsBars: { dataKey: string; fill: string; stackId?: string }[];
294
+ } => {
295
+ const { rechartsBars, barDataKeys } = getRechartsBarsAndBarDataKeys(
296
+ bars,
297
+ colorSet,
298
+ stacked,
299
+ );
300
+
301
+ let data =
302
+ type !== 'category' && type.type === 'time'
303
+ ? transformTimeData(bars, type, barDataKeys)
304
+ : transformCategoryData(bars, barDataKeys);
305
+
306
+ if (type === 'category' && defaultSort) {
307
+ data = applySortingToData(data, barDataKeys, defaultSort);
308
+ }
309
+
310
+ const sortedRechartsBars = sortStackedBars(rechartsBars, data, stacked);
311
+
312
+ return {
313
+ rechartsBars: sortedRechartsBars,
314
+ data,
315
+ };
316
+ };
317
+
318
+ export type UnitRange = {
319
+ threshold: number;
320
+ label: string;
321
+ }[];
322
+
323
+ export const computeUnitLabelAndRoundReferenceValue = (
324
+ data: any,
325
+ maxValue: number,
326
+ unitRange: UnitRange | undefined,
327
+ ) => {
328
+ if (!unitRange) {
329
+ const roundReferenceValue = getRoundReferenceValue(maxValue);
330
+ return { unitLabel: '', roundReferenceValue, rechartsData: data };
331
+ }
332
+
333
+ const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue);
334
+ const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
335
+ const roundReferenceValue = getRoundReferenceValue(topValue);
336
+ const rechartsData = data.map((dataPoint) => {
337
+ const normalizedDataPoint = { ...dataPoint };
338
+ Object.entries(dataPoint).forEach(([key, value]) => {
339
+ if (key !== 'category' && typeof value === 'number') {
340
+ normalizedDataPoint[key] = value / valueBase;
341
+ }
342
+ });
343
+ return normalizedDataPoint;
344
+ });
345
+ return { unitLabel, roundReferenceValue, rechartsData };
346
+ };
347
+
348
+ /**
349
+ * Return the unit label base on the current dataset, and the valueBase which is used to convert the data
350
+ * @param {any} unitRange
351
+ * @param {any} maxValue the maximum value among the data set
352
+ * @returns {any}
353
+ */
354
+ export function getUnitLabel(
355
+ unitRange: {
356
+ threshold: number;
357
+ label: string;
358
+ }[],
359
+ maxValue: number,
360
+ ): {
361
+ valueBase: number;
362
+ unitLabel: string;
363
+ } {
364
+ // first sort the unitRange
365
+ unitRange.sort(
366
+ (
367
+ unitA: {
368
+ threshold: number;
369
+ label: string;
370
+ },
371
+ unitB: {
372
+ threshold: number;
373
+ label: string;
374
+ },
375
+ ) => {
376
+ return unitA.threshold - unitB.threshold;
377
+ },
378
+ );
379
+ let index = unitRange.findIndex((range) => range.threshold > maxValue);
380
+
381
+ // last unit
382
+ if (index === -1) {
383
+ index = unitRange.length;
384
+ }
385
+
386
+ if (index === 0) {
387
+ return {
388
+ valueBase: unitRange[index].threshold,
389
+ unitLabel: unitRange[index].label,
390
+ };
391
+ }
392
+
393
+ return {
394
+ // if the threshold is 0, we use 1 as the value base to avoid division by 0
395
+ valueBase: unitRange[index - 1].threshold || 1,
396
+ unitLabel: unitRange[index - 1].label,
397
+ };
398
+ }
399
+
400
+ // Sort stacked bars by their average values in descending order
401
+ // This ensures the largest bars appear at the bottom of the stack
402
+ export const sortStackedBars = (
403
+ rechartsBars: {
404
+ dataKey: string;
405
+ fill: string;
406
+ stackId?: string;
407
+ }[],
408
+ data: {
409
+ [key: string]: string | number;
410
+ }[],
411
+ stacked?: boolean,
412
+ ) => {
413
+ if (!stacked) {
414
+ return rechartsBars;
415
+ }
416
+ const barAverages = rechartsBars.map((bar) => {
417
+ const values = data
418
+ .map((item) => Number(item[bar.dataKey]) || 0)
419
+ .filter((value) => !isNaN(value));
420
+ const average =
421
+ values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
422
+ return { ...bar, average };
423
+ });
424
+
425
+ // Sort by average in descending order (largest first, which will be at bottom in stack)
426
+ barAverages.sort((a, b) => b.average - a.average);
427
+ // Remove the average property and keep only the bar data
428
+ return barAverages.map(({ average, ...bar }) => bar);
429
+ };
430
+
431
+ export const renderTooltipContent = <T extends BarchartBars>(
432
+ props: TooltipContentProps<number, string>,
433
+ tooltip: BarchartTooltipFn<T> | undefined,
434
+ hoveredValue: string | undefined,
435
+ ) => {
436
+ const { active, payload, label } = props;
437
+
438
+ if (!active || !tooltip) {
439
+ return null;
440
+ }
441
+
442
+ const tooltipValues: {
443
+ label: T[number]['label'];
444
+ value: number;
445
+ isHovered: boolean;
446
+ }[] = payload.map((item) => ({
447
+ label: item.name,
448
+ value: item.value,
449
+ isHovered: item.name === hoveredValue,
450
+ }));
451
+
452
+ const currentPoint = {
453
+ category: label as string | number,
454
+ values: tooltipValues,
455
+ };
456
+
457
+ return tooltip(currentPoint);
458
+ };
459
+
460
+ /**
461
+ * Filters both chart data and recharts bars to only include selected resources from legend
462
+ * @param data - Array of chart data objects with category and resource values
463
+ * @param rechartsBars - Array of recharts bar configurations
464
+ * @param selectedResources - Array of selected resource names
465
+ * @returns Object containing filtered data and recharts bars
466
+ */
467
+ export const filterChartDataAndBarsByLegendSelection = (
468
+ data: { [key: string]: string | number }[],
469
+ rechartsBars: { dataKey: string; fill: string; stackId?: string }[],
470
+ selectedResources: string[],
471
+ ) => {
472
+ // If no resources are selected, show all data and bars (default behavior)
473
+ if (selectedResources.length === 0) {
474
+ return { filteredData: data, filteredRechartsBars: rechartsBars };
475
+ }
476
+
477
+ // Filter recharts bars
478
+ const filteredRechartsBars = rechartsBars.filter((bar) =>
479
+ selectedResources.includes(bar.dataKey),
480
+ );
481
+
482
+ // Filter data to only include selected resources
483
+ const filteredData = data.map((item) => {
484
+ const filteredItem: { [key: string]: string | number } = {
485
+ category: item.category,
486
+ };
487
+ selectedResources.forEach((resource) => {
488
+ if (resource in item) {
489
+ filteredItem[resource] = item[resource];
490
+ }
491
+ });
492
+ return filteredItem;
493
+ });
494
+
495
+ return { filteredData, filteredRechartsBars };
496
+ };
497
+
498
+ export const useChartData = <T extends BarchartBars>(
499
+ bars: T,
500
+ type: BarchartProps<T>['type'],
501
+ colorSet: Record<string, ChartColors | string>,
502
+ stacked?: boolean,
503
+ defaultSort?: BarchartProps<T>['defaultSort'],
504
+ unitRange?: UnitRange,
505
+ ) => {
506
+ const { selectedResources } = useChartLegend();
507
+ const { data, rechartsBars } = formatPrometheusDataToRechartsDataAndBars(
508
+ bars,
509
+ type,
510
+ colorSet,
511
+ stacked,
512
+ defaultSort,
513
+ );
514
+
515
+ // Filter both data and bars to only include selected resources for accurate maxValue calculation
516
+ const { filteredData, filteredRechartsBars } =
517
+ filterChartDataAndBarsByLegendSelection(
518
+ data,
519
+ rechartsBars,
520
+ selectedResources,
521
+ );
522
+
523
+ const maxValue = getMaxBarValue(filteredData, stacked);
524
+
525
+ const { unitLabel, roundReferenceValue, rechartsData } =
526
+ computeUnitLabelAndRoundReferenceValue(filteredData, maxValue, unitRange);
527
+
528
+ return {
529
+ rechartsBars: filteredRechartsBars,
530
+ unitLabel,
531
+ roundReferenceValue,
532
+ rechartsData,
533
+ };
534
+ };
@@ -1,4 +1,4 @@
1
- import React, { ButtonHTMLAttributes } from 'react';
1
+ import { ButtonHTMLAttributes } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
  import { spacing } from '../../spacing';
4
4
  import { fontSize, fontWeight } from '../../style/theme';
@@ -0,0 +1,113 @@
1
+ import styled from 'styled-components';
2
+ import { useChartLegend } from './ChartLegendWrapper';
3
+ import { Text } from '../text/Text.component';
4
+ import { chartColors } from '../../style/theme';
5
+ import { useCallback } from 'react';
6
+
7
+ type ChartLegendProps = {
8
+ shape: 'line' | 'rectangle';
9
+ disabled?: boolean;
10
+ direction?: 'horizontal' | 'vertical';
11
+ };
12
+
13
+ const Legend = styled.div<{ direction: 'horizontal' | 'vertical' }>`
14
+ display: flex;
15
+ flex-direction: ${({ direction }) =>
16
+ direction === 'horizontal' ? 'row' : 'column'};
17
+ gap: ${({ direction }) => (direction === 'horizontal' ? '16px' : '8px')};
18
+ flex-wrap: wrap;
19
+ `;
20
+
21
+ const LegendItem = styled.div<{ disabled?: boolean; selected?: boolean }>`
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 8px;
25
+ cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
26
+ opacity: ${({ selected, disabled }) => (disabled ? 0.5 : selected ? 1 : 0.7)};
27
+ transition: opacity 0.2s ease;
28
+
29
+ &:hover {
30
+ opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
31
+ }
32
+ `;
33
+
34
+ const LegendShape = styled.div<{
35
+ color?: string;
36
+ shape: 'line' | 'rectangle';
37
+ chartColors: Record<string, string>;
38
+ }>`
39
+ ${({ shape, color, chartColors }) => {
40
+ if (shape === 'line') {
41
+ return `
42
+ width: 20px;
43
+ height: 2px;
44
+ background-color: ${chartColors[color as keyof typeof chartColors] || color};
45
+ `;
46
+ } else if (shape === 'rectangle') {
47
+ return `
48
+ width: 12px;
49
+ height: 12px;
50
+ background-color: ${chartColors[color as keyof typeof chartColors] || color};
51
+ border-radius: 2px;
52
+ `;
53
+ } else {
54
+ console.error(
55
+ 'The shape is not valid. Please use "line" or "rectangle".',
56
+ );
57
+ }
58
+ }}
59
+ `;
60
+
61
+ export const ChartLegend = ({
62
+ shape,
63
+ disabled = false,
64
+ direction = 'horizontal',
65
+ }: ChartLegendProps) => {
66
+ const {
67
+ listResources,
68
+ getColor,
69
+ isSelected,
70
+ addSelectedResource,
71
+ removeSelectedResource,
72
+ } = useChartLegend();
73
+
74
+ const resources = listResources();
75
+
76
+ const handleLegendClick = useCallback(
77
+ (resource: string) => {
78
+ if (disabled) return;
79
+
80
+ if (isSelected(resource)) {
81
+ removeSelectedResource(resource);
82
+ } else {
83
+ addSelectedResource(resource);
84
+ }
85
+ },
86
+ [disabled, isSelected, addSelectedResource, removeSelectedResource],
87
+ );
88
+
89
+ return (
90
+ <Legend direction={direction}>
91
+ {resources.map((resource) => {
92
+ const color = getColor(resource);
93
+ const selected = isSelected(resource);
94
+
95
+ return (
96
+ <LegendItem
97
+ key={resource}
98
+ disabled={disabled}
99
+ selected={selected}
100
+ onClick={() => handleLegendClick(resource)}
101
+ >
102
+ <LegendShape
103
+ color={color}
104
+ shape={shape}
105
+ chartColors={chartColors}
106
+ />
107
+ <Text variant="Basic">{resource}</Text>
108
+ </LegendItem>
109
+ );
110
+ })}
111
+ </Legend>
112
+ );
113
+ };