@bundle-stats/ui 4.13.0-beta.16 → 4.13.0-beta.18

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 (91) hide show
  1. package/lib/assets/icons.svg +70 -67
  2. package/lib/assets/icons.svg.js +14 -12
  3. package/lib/assets/icons.svg.js.map +1 -1
  4. package/lib/components/bundle-assets/bundle-assets.js +39 -7
  5. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  6. package/lib/components/bundle-assets-totals/bundle-assets-totals.js +4 -4
  7. package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  8. package/lib/components/bundle-modules/bundle-modules.js +28 -7
  9. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  10. package/lib/components/bundle-packages/bundle-packages.js +39 -7
  11. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  12. package/lib/components/metrics-display-selector/metrics-display-selector.js +41 -4
  13. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  14. package/lib/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  15. package/lib/components/metrics-treemap/index.js +4 -0
  16. package/lib/components/metrics-treemap/index.js.map +1 -1
  17. package/lib/components/metrics-treemap/metrics-treemap.constants.js +3 -0
  18. package/lib/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  19. package/lib/components/metrics-treemap/metrics-treemap.js +63 -44
  20. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  21. package/lib/components/metrics-treemap/metrics-treemap.module.css +60 -28
  22. package/lib/components/metrics-treemap/metrics-treemap.utils.js +70 -0
  23. package/lib/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  24. package/lib/hooks/metrics-display-type.js +56 -24
  25. package/lib/hooks/metrics-display-type.js.map +1 -1
  26. package/lib/hooks/search-params.js +1 -0
  27. package/lib/hooks/search-params.js.map +1 -1
  28. package/lib/ui/icon/icon.js +1 -0
  29. package/lib/ui/icon/icon.js.map +1 -1
  30. package/lib/ui/input-search/input-search.js +1 -1
  31. package/lib/ui/input-search/input-search.js.map +1 -1
  32. package/lib-esm/assets/icons.svg +70 -67
  33. package/lib-esm/assets/icons.svg.js +14 -12
  34. package/lib-esm/assets/icons.svg.js.map +1 -1
  35. package/lib-esm/components/bundle-assets/bundle-assets.js +40 -8
  36. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  37. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +5 -5
  38. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
  39. package/lib-esm/components/bundle-modules/bundle-modules.js +29 -8
  40. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  41. package/lib-esm/components/bundle-packages/bundle-packages.js +40 -8
  42. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  43. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +41 -4
  44. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  45. package/lib-esm/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  46. package/lib-esm/components/metrics-treemap/index.js +1 -0
  47. package/lib-esm/components/metrics-treemap/index.js.map +1 -1
  48. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js +2 -0
  49. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
  50. package/lib-esm/components/metrics-treemap/metrics-treemap.js +64 -45
  51. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  52. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +60 -28
  53. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js +65 -0
  54. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
  55. package/lib-esm/hooks/metrics-display-type.js +57 -24
  56. package/lib-esm/hooks/metrics-display-type.js.map +1 -1
  57. package/lib-esm/hooks/search-params.js +1 -0
  58. package/lib-esm/hooks/search-params.js.map +1 -1
  59. package/lib-esm/ui/icon/icon.js +1 -0
  60. package/lib-esm/ui/icon/icon.js.map +1 -1
  61. package/lib-esm/ui/input-search/input-search.js +1 -1
  62. package/lib-esm/ui/input-search/input-search.js.map +1 -1
  63. package/package.json +7 -7
  64. package/src/assets/icons/check.svg +4 -0
  65. package/src/assets/icons.svg +70 -67
  66. package/src/assets/icons.svg.jsx +12 -12
  67. package/src/components/bundle-assets/bundle-assets.jsx +76 -20
  68. package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +5 -4
  69. package/src/components/bundle-modules/bundle-modules.tsx +76 -16
  70. package/src/components/bundle-packages/bundle-packages.jsx +71 -16
  71. package/src/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
  72. package/src/components/metrics-display-selector/metrics-display-selector.stories.tsx +33 -0
  73. package/src/components/metrics-display-selector/metrics-display-selector.tsx +83 -25
  74. package/src/components/metrics-treemap/__tests__/metrics-treemap.utils.ts +143 -0
  75. package/src/components/metrics-treemap/index.ts +1 -0
  76. package/src/components/metrics-treemap/metrics-treemap.constants.ts +43 -0
  77. package/src/components/metrics-treemap/metrics-treemap.module.css +60 -28
  78. package/src/components/metrics-treemap/metrics-treemap.stories.tsx +44 -10
  79. package/src/components/metrics-treemap/metrics-treemap.tsx +258 -90
  80. package/src/components/metrics-treemap/metrics-treemap.utils.ts +107 -0
  81. package/src/hooks/metrics-display-type.ts +87 -29
  82. package/src/hooks/search-params.js +1 -0
  83. package/src/ui/icon/icon.tsx +1 -0
  84. package/src/ui/input-search/input-search.tsx +1 -0
  85. package/types/components/metrics-display-selector/metrics-display-selector.d.ts +3 -1
  86. package/types/components/metrics-treemap/index.d.ts +1 -0
  87. package/types/components/metrics-treemap/metrics-treemap.constants.d.ts +15 -0
  88. package/types/components/metrics-treemap/metrics-treemap.d.ts +7 -5
  89. package/types/components/metrics-treemap/metrics-treemap.utils.d.ts +4 -0
  90. package/types/hooks/metrics-display-type.d.ts +8 -2
  91. package/types/ui/icon/icon.d.ts +2 -0
@@ -1,24 +1,34 @@
1
- import React, { RefObject, forwardRef, useCallback, useMemo, useRef } from 'react';
1
+ import React, {
2
+ type ComponentProps,
3
+ type MouseEventHandler,
4
+ type ReactNode,
5
+ type Ref,
6
+ type RefObject,
7
+ forwardRef,
8
+ useCallback,
9
+ useMemo,
10
+ useRef,
11
+ } from 'react';
2
12
  import { useDebounce, useHoverDirty, useMeasure, useMouseHovered } from 'react-use';
3
13
  import cx from 'classnames';
4
- import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
5
- import { hierarchy, treemap, treemapSquarify } from 'd3';
14
+ import { HierarchyRectangularNode, hierarchy, treemap, treemapSquarify } from 'd3';
6
15
  import { Tooltip, TooltipArrow, TooltipAnchor, useTooltipState } from 'ariakit/tooltip';
16
+ import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
7
17
 
8
18
  import { Stack } from '../../layout/stack';
9
19
  import { FileName } from '../../ui/file-name';
10
20
  import { Delta } from '../delta';
11
21
  import { RunInfo } from '../run-info';
22
+ import type { TreeLeaf, TreeNode, Tree } from './metrics-treemap.constants';
12
23
  import css from './metrics-treemap.module.css';
13
24
 
14
25
  const SQUARIFY_RATIO = 1.33;
15
- const PADDING_OUTER = 0;
16
- const PADDING_INNER = 2;
17
-
18
- type TileSizeDisplay = 'minimal' | 'small' | 'default';
26
+ const PADDING_OUTER = 1;
27
+ const PADDING_INNER = 1;
28
+ const GROUPED_PADDING_TOP = 16;
19
29
 
20
30
  /**
21
- * Resolve the tile size using predefined values to avoid
31
+ * Resolve the tile's size using predefined values to avoid
22
32
  * computing the size of the content for every tile
23
33
  */
24
34
  const PADDING_TOP = 4;
@@ -29,6 +39,20 @@ const VERTICAL_SPACING = 4;
29
39
  const LINE_HEIGHT = 16;
30
40
  const LINE_HEIGHT_SMALL = 13.3;
31
41
 
42
+ function resolveTileGroupSizeDisplay(width: number, height: number): TileSizeDisplay {
43
+ if (height < GROUPED_PADDING_TOP || width < 24) {
44
+ return 'minimal';
45
+ }
46
+
47
+ if (height < GROUPED_PADDING_TOP + 8) {
48
+ return 'small';
49
+ }
50
+
51
+ return 'default';
52
+ }
53
+
54
+ type TileSizeDisplay = 'minimal' | 'small' | 'default';
55
+
32
56
  function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
33
57
  if (
34
58
  height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
@@ -73,13 +97,26 @@ const TileTooltipContent = (props: TileTooltipContentProps) => {
73
97
  };
74
98
 
75
99
  interface TileContentProps {
76
- sizeDisplay: 'minimal' | 'small' | 'default';
100
+ /**
101
+ * Node label
102
+ */
103
+ label: string;
104
+ /**
105
+ * Tile id - metric label or basename for grouped tiles
106
+ */
77
107
  item: ReportMetricRow;
108
+ /**
109
+ * The estimated size of the tile
110
+ */
111
+ sizeDisplay: 'minimal' | 'small' | 'default';
112
+ /**
113
+ * Metric run info
114
+ */
78
115
  runInfo: MetricRunInfo;
79
116
  }
80
117
 
81
- const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivElement>) => {
82
- const { sizeDisplay, item, runInfo } = props;
118
+ const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement>) => {
119
+ const { label, sizeDisplay, item, runInfo } = props;
83
120
 
84
121
  if (sizeDisplay === 'minimal') {
85
122
  return <div className={css.tileContent} ref={ref} />;
@@ -87,12 +124,12 @@ const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivE
87
124
 
88
125
  return (
89
126
  <div className={css.tileContent} ref={ref}>
90
- <FileName as="p" className={css.leadContentLabel} name={item.label} />
127
+ <FileName as="p" className={css.tileContentLabel} name={label || item.label} />
91
128
  {sizeDisplay !== 'small' && (
92
- <p className={css.leadContentValue}>
93
- <span className={css.leadContentMetric}>{runInfo.displayValue}</span>
129
+ <p className={css.tileContentValue}>
130
+ <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
94
131
  <Delta
95
- className={css.leadContentDelta}
132
+ className={css.tileContentDelta}
96
133
  displayValue={runInfo.displayDeltaPercentage}
97
134
  deltaType={runInfo.deltaType}
98
135
  />
@@ -103,7 +140,7 @@ const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivE
103
140
  });
104
141
 
105
142
  const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject<Element> }) => {
106
- const { sizeDisplay, item, runInfo, parentRef } = props;
143
+ const { label, sizeDisplay, item, runInfo, parentRef } = props;
107
144
 
108
145
  const pointer = useMouseHovered(parentRef, { whenHovered: true });
109
146
 
@@ -132,7 +169,7 @@ const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject
132
169
  return (
133
170
  <>
134
171
  <TooltipAnchor state={tooltipState} className={css.tileContentTooltipAnchor}>
135
- <TileContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
172
+ <TileContent label={label} sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
136
173
  </TooltipAnchor>
137
174
  <Tooltip state={tooltipState} className={css.tooltip}>
138
175
  <TooltipArrow state={tooltipState} size={16} className={css.tileTooltipArrow} />
@@ -142,36 +179,29 @@ const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject
142
179
  );
143
180
  };
144
181
 
145
- interface TreeNode {
146
- id: string;
147
- value: number;
148
- item: ReportMetricRow;
149
- children?: Array<TreeNode>;
150
- }
151
-
152
- interface RootTree {
153
- id: string;
154
- value: number;
155
- children: Array<TreeNode>;
156
- }
157
-
158
182
  interface TileProps {
159
183
  left: number;
160
184
  top: number;
161
185
  width: number;
162
186
  height: number;
163
- data: TreeNode;
187
+ data: TreeLeaf;
164
188
  onClick?: (id: string) => void;
165
189
  }
166
190
 
167
191
  const Tile = (props: TileProps) => {
168
192
  const { left, top, width, height, data, onClick } = props;
169
193
 
170
- const { item } = data;
194
+ const { item, label } = data;
171
195
  const runInfo = item.runs?.[0] as MetricRunInfo;
172
196
 
173
197
  const sizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
174
- const handleOnClick = useCallback(() => onClick?.(item.key), [item.key, onClick]);
198
+ const handleOnClick: MouseEventHandler<HTMLButtonElement> = useCallback(
199
+ (event) => {
200
+ event.stopPropagation();
201
+ onClick?.(item.key);
202
+ },
203
+ [item.key, onClick],
204
+ );
175
205
 
176
206
  const contentRef = useRef<HTMLButtonElement>(null);
177
207
  const hover = useHoverDirty(contentRef);
@@ -179,7 +209,8 @@ const Tile = (props: TileProps) => {
179
209
  const className = cx(
180
210
  css.tile,
181
211
  css[`tile-${runInfo.deltaType}`],
182
- sizeDisplay === 'small' && css.tileSmall,
212
+ sizeDisplay === 'small' && css.tileSizeSmall,
213
+ sizeDisplay === 'default' && css.tileSizeDefault,
183
214
  left === 0 && css.tileFirstCol,
184
215
  );
185
216
 
@@ -193,106 +224,243 @@ const Tile = (props: TileProps) => {
193
224
  >
194
225
  {hover ? (
195
226
  <TileContentWithTooltip
227
+ label={label}
196
228
  sizeDisplay={sizeDisplay}
197
229
  item={item}
198
230
  runInfo={runInfo}
199
231
  parentRef={contentRef}
200
232
  />
201
233
  ) : (
202
- <TileContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
234
+ <TileContent label={label} sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
203
235
  )}
204
236
  </button>
205
237
  );
238
+ /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */
239
+ };
240
+
241
+ interface TileGroupProps extends ComponentProps<'div'> {
242
+ /**
243
+ * Node title
244
+ */
245
+ title?: string;
246
+ /**
247
+ * Node id
248
+ */
249
+ id: string;
250
+ /**
251
+ * Node children
252
+ */
253
+ childNodes: HierarchyRectangularNode<TreeNode>['children'];
254
+ /**
255
+ * On click tile handler
256
+ */
257
+ onItemClick: TileProps['onClick'];
258
+ /**
259
+ * On click group title
260
+ */
261
+ onGroupClick?: (groupId: string) => void;
262
+
263
+ /**
264
+ * Node rectangle dimensions
265
+ */
266
+ width?: number;
267
+ height?: number;
268
+
269
+ /**
270
+ * Node rectangle relative coordinates (relative to parent)
271
+ */
272
+ left: number;
273
+ top: number;
274
+
275
+ /**
276
+ * Node rectangle absolute coordinates (relative to canvas)
277
+ */
278
+ absoluteLeft: number;
279
+ absoluteTop: number;
280
+ }
281
+
282
+ const TileGroup = (props: TileGroupProps) => {
283
+ const {
284
+ title = '',
285
+ id,
286
+ childNodes,
287
+ onItemClick,
288
+ onGroupClick,
289
+ left,
290
+ top,
291
+ width,
292
+ height,
293
+ absoluteLeft,
294
+ absoluteTop,
295
+ } = props;
296
+
297
+ const onClick: MouseEventHandler<HTMLDivElement> = useCallback(
298
+ (event) => {
299
+ onGroupClick?.(id);
300
+ event.stopPropagation();
301
+ },
302
+ [onGroupClick, id],
303
+ );
304
+
305
+ const displaySize = useMemo(
306
+ () => resolveTileGroupSizeDisplay(width || 0, height || 0),
307
+ [width, height],
308
+ );
309
+
310
+ if (title && displaySize === 'minimal') {
311
+ return (
312
+ <div
313
+ onClick={onClick}
314
+ role="button"
315
+ aria-label="View children entries"
316
+ className={css.tileGroup}
317
+ style={{ left, top, width, height }}
318
+ />
319
+ );
320
+ }
321
+
322
+ if (title && displaySize === 'small') {
323
+ return (
324
+ <div
325
+ onClick={onClick}
326
+ role="button"
327
+ aria-label="View children entries"
328
+ style={{ left, top, width, height }}
329
+ className={css.tileGroup}
330
+ >
331
+ <div className={css.tileGroupTitle}>{title}</div>
332
+ </div>
333
+ );
334
+ }
335
+
336
+ return (
337
+ <div
338
+ onClick={onClick}
339
+ role="button"
340
+ aria-label="View children entries"
341
+ className={cx(css.tileGroup, css.tileGroupSizeDefault)}
342
+ style={{ left, top, width, height }}
343
+ >
344
+ {title && <div className={css.tileGroupTitle}>{title}</div>}
345
+ {childNodes?.map((childNode) => {
346
+ if ('children' in childNode) {
347
+ return (
348
+ <TileGroup
349
+ title={childNode.data.label}
350
+ id={childNode.data.id}
351
+ childNodes={childNode.children}
352
+ left={childNode.x0 - absoluteLeft}
353
+ top={childNode.y0 - absoluteTop}
354
+ absoluteLeft={childNode.x0}
355
+ absoluteTop={childNode.y0}
356
+ width={childNode.x1 - childNode.x0}
357
+ height={childNode.y1 - childNode.y0}
358
+ onItemClick={onItemClick}
359
+ onGroupClick={onGroupClick}
360
+ key={childNode.data.id}
361
+ />
362
+ );
363
+ }
364
+
365
+ return (
366
+ <Tile
367
+ data={childNode.data as TreeLeaf}
368
+ left={childNode.x0 - absoluteLeft}
369
+ top={childNode.y0 - absoluteTop}
370
+ width={childNode.x1 - childNode.x0}
371
+ height={childNode.y1 - childNode.y0}
372
+ onClick={onItemClick}
373
+ key={childNode.data.id}
374
+ />
375
+ );
376
+ })}
377
+ </div>
378
+ );
206
379
  };
207
380
 
208
381
  interface UseMetricsTreemapHierarchyParams {
209
- items: Array<ReportMetricRow>;
382
+ treeNodes: Tree;
210
383
  width: number;
211
384
  height: number;
385
+ nested: boolean;
212
386
  }
213
387
 
214
388
  function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
215
- const { items, width, height } = params;
389
+ const { treeNodes, width, height, nested } = params;
216
390
 
217
391
  const treemapHierarchy = useMemo(() => {
218
- const children = items.map((item) => {
219
- /**
220
- * Set tile value to the largest run value to allow to
221
- * display the impact of deleted/new entries
222
- */
223
- const values = item.runs.map((run) => run?.value || 0);
224
- const value = Math.max(...values);
225
-
226
- return {
227
- id: item.key,
228
- value,
229
- item,
230
- };
231
- });
232
-
233
- const treemapData = {
234
- id: 'root',
235
- name: 'root',
236
- value: 0,
237
- children,
238
- };
392
+ const customHierarchy = hierarchy<Tree>(treeNodes);
239
393
 
240
- const customHierarchy = hierarchy<RootTree>(treemapData);
241
394
  customHierarchy.sum((node) => node.value);
395
+ // sort by value descending for all cases
242
396
  // @ts-expect-error
243
397
  customHierarchy.sort((a, b) => b.value - a.value);
244
398
 
245
399
  return customHierarchy;
246
- }, [items]);
400
+ }, [treeNodes]);
247
401
 
248
- const tiles = useMemo(() => {
249
- const createTremapLayout = treemap()
402
+ const treemapNodes = useMemo(() => {
403
+ let createTremapLayout = treemap()
250
404
  .size([width, height])
251
405
  .tile(treemapSquarify.ratio(SQUARIFY_RATIO))
252
406
  .paddingOuter(PADDING_OUTER)
253
407
  .paddingInner(PADDING_INNER);
254
- // @ts-expect-error
255
- const tremapLayout = createTremapLayout(treemapHierarchy);
256
408
 
257
- return tremapLayout.leaves().filter((tile) => Boolean(tile.parent));
409
+ // Add padding top for group title
410
+ if (nested) {
411
+ createTremapLayout = createTremapLayout.paddingTop(GROUPED_PADDING_TOP);
412
+ }
413
+
414
+ // @ts-expect-error
415
+ return createTremapLayout(treemapHierarchy) as HierarchyRectangularNode<TreeNode>;
258
416
  }, [height, width, treemapHierarchy]);
259
417
 
260
- return tiles;
418
+ return treemapNodes;
261
419
  }
262
420
 
263
421
  interface MetricsTreemapProps {
264
- items: Array<ReportMetricRow>;
265
- emptyMessage?: React.ReactNode;
422
+ treeNodes: Tree;
423
+ emptyMessage?: ReactNode;
424
+ nested?: boolean;
266
425
  onItemClick?: (entryId: string) => void;
426
+ onGroupClick?: (entryId: string) => void;
267
427
  }
268
428
 
269
- export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
270
- const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
429
+ export const MetricsTreemap = (props: MetricsTreemapProps & ComponentProps<'div'>) => {
430
+ const {
431
+ className = '',
432
+ treeNodes,
433
+ onItemClick,
434
+ onGroupClick,
435
+ emptyMessage = 'No data',
436
+ nested = false,
437
+ ...restProps
438
+ } = props;
271
439
 
272
440
  const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
273
- const tiles = useMetricsTreemapHierarchy({ items, width, height });
441
+ const rootNode = useMetricsTreemapHierarchy({ treeNodes, width, height, nested });
274
442
 
275
443
  return (
276
- <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
277
- {tiles.length > 0 ? (
444
+ <div
445
+ className={cx(css.root, nested && css.nested, className)}
446
+ {...restProps}
447
+ ref={containerRef}
448
+ >
449
+ {rootNode.children && rootNode.children.length > 0 ? (
278
450
  <div className={css.canvas}>
279
- {tiles.map((tile) => {
280
- if (!tile.parent) {
281
- return null;
282
- }
283
-
284
- return (
285
- <Tile
286
- data={tile.data as TreeNode}
287
- left={tile.x0}
288
- top={tile.y0}
289
- width={tile.x1 - tile.x0}
290
- height={tile.y1 - tile.y0}
291
- onClick={onItemClick}
292
- key={tile.id}
293
- />
294
- );
295
- })}
451
+ <TileGroup
452
+ title={nested ? rootNode.data.label : undefined}
453
+ id={rootNode.data.id}
454
+ childNodes={rootNode.children}
455
+ onItemClick={onItemClick}
456
+ onGroupClick={onGroupClick}
457
+ left={rootNode.x0}
458
+ top={rootNode.y0}
459
+ absoluteLeft={rootNode.x0}
460
+ absoluteTop={rootNode.y0}
461
+ width={rootNode.x1 - rootNode.x0}
462
+ height={rootNode.y1 - rootNode.y0}
463
+ />
296
464
  </div>
297
465
  ) : (
298
466
  <div className={css.emptyMessage}>
@@ -0,0 +1,107 @@
1
+ import type { ReportMetricRow } from '@bundle-stats/utils';
2
+
3
+ import type { TreeLeaf, TreeNodeChildren, Tree } from './metrics-treemap.constants';
4
+
5
+ const ROOT_LABEL = '(root)';
6
+
7
+ /**
8
+ * Set node value to the largest run value to allow to
9
+ * display the impact of deleted/new entries
10
+ */
11
+ function getReportMetricRowMaxValue(item: ReportMetricRow): number {
12
+ const values = item.runs.map((run) => run?.value || 0);
13
+ return Math.max(...values);
14
+ }
15
+
16
+ /**
17
+ * Generate treemap node list(flat)
18
+ */
19
+ export function getTreemapNodes(items: Array<ReportMetricRow>): Tree {
20
+ const rootChildren: Array<TreeLeaf> = items.map((item) => ({
21
+ id: item.key,
22
+ label: item.key,
23
+ value: getReportMetricRowMaxValue(item),
24
+ item,
25
+ }));
26
+
27
+ return {
28
+ id: '',
29
+ label: ROOT_LABEL,
30
+ value: 0,
31
+ children: rootChildren,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Recursively set treemap nodes
37
+ */
38
+ function setTreeNode(
39
+ nodes: TreeNodeChildren,
40
+ slugs: Array<string>,
41
+ currentSlugId: number,
42
+ newNode: TreeLeaf,
43
+ ): void {
44
+ const baseSlugs = slugs.slice(0, currentSlugId);
45
+ const [currentSlug, ...restSlug] = slugs.slice(currentSlugId);
46
+
47
+ // Add to current nodes if there are no child nodes
48
+ if (!currentSlug) {
49
+ nodes.push(newNode);
50
+ return;
51
+ }
52
+
53
+ const currentNodePath = [...baseSlugs, currentSlug].join('/');
54
+
55
+ // Check for existing parent in current nodes
56
+ let parentNode = nodes.find((treeNode) => treeNode.id === currentNodePath) as Tree;
57
+
58
+ // Create the new parentNode if missing
59
+ if (!parentNode) {
60
+ parentNode = {
61
+ id: currentNodePath,
62
+ label: currentSlug,
63
+ value: 0,
64
+ children: [],
65
+ };
66
+
67
+ nodes.push(parentNode);
68
+ }
69
+
70
+ // If there are no other slugs, the new node is as leaf and we add it to the parent
71
+ if (restSlug.length === 0) {
72
+ parentNode.children.push(newNode);
73
+ return;
74
+ }
75
+
76
+ setTreeNode(parentNode.children, slugs, currentSlugId + 1, newNode);
77
+ }
78
+
79
+ /**
80
+ * Generate treemap nodes tree by directory
81
+ */
82
+ export function getTreemapNodesGroupedByPath(items: Array<ReportMetricRow>): Tree {
83
+ const treeNodes: TreeNodeChildren = [];
84
+
85
+ items.forEach((item) => {
86
+ // const normalizedPath = item.key.replace(/^\.?\//, ''); // replace './' or '/' prefix
87
+ const slugs = item.key.split('/');
88
+ const baseSlugs = slugs.slice(0, -1);
89
+ const baseName = slugs.slice(-1)[0];
90
+
91
+ const treeNode = {
92
+ id: item.key,
93
+ label: baseName,
94
+ value: getReportMetricRowMaxValue(item),
95
+ item,
96
+ };
97
+
98
+ setTreeNode(treeNodes, baseSlugs, 0, treeNode);
99
+ });
100
+
101
+ return {
102
+ id: '',
103
+ label: ROOT_LABEL,
104
+ value: 0,
105
+ children: treeNodes,
106
+ };
107
+ }