@bundle-stats/ui 4.13.0-beta.16 → 4.13.0-beta.17
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.
- package/lib/assets/icons.svg +70 -67
- package/lib/assets/icons.svg.js +14 -12
- package/lib/assets/icons.svg.js.map +1 -1
- package/lib/components/bundle-assets/bundle-assets.js +39 -7
- package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
- package/lib/components/bundle-assets-totals/bundle-assets-totals.js +4 -4
- package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
- package/lib/components/bundle-modules/bundle-modules.js +28 -7
- package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
- package/lib/components/bundle-packages/bundle-packages.js +39 -7
- package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
- package/lib/components/metrics-display-selector/metrics-display-selector.js +41 -4
- package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
- package/lib/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
- package/lib/components/metrics-treemap/index.js +4 -0
- package/lib/components/metrics-treemap/index.js.map +1 -1
- package/lib/components/metrics-treemap/metrics-treemap.constants.js +3 -0
- package/lib/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
- package/lib/components/metrics-treemap/metrics-treemap.js +57 -38
- package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
- package/lib/components/metrics-treemap/metrics-treemap.module.css +55 -25
- package/lib/components/metrics-treemap/metrics-treemap.utils.js +70 -0
- package/lib/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
- package/lib/hooks/metrics-display-type.js +56 -24
- package/lib/hooks/metrics-display-type.js.map +1 -1
- package/lib/ui/icon/icon.js +1 -0
- package/lib/ui/icon/icon.js.map +1 -1
- package/lib-esm/assets/icons.svg +70 -67
- package/lib-esm/assets/icons.svg.js +14 -12
- package/lib-esm/assets/icons.svg.js.map +1 -1
- package/lib-esm/components/bundle-assets/bundle-assets.js +40 -8
- package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
- package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +5 -5
- package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -1
- package/lib-esm/components/bundle-modules/bundle-modules.js +29 -8
- package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
- package/lib-esm/components/bundle-packages/bundle-packages.js +40 -8
- package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
- package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +41 -4
- package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
- package/lib-esm/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
- package/lib-esm/components/metrics-treemap/index.js +1 -0
- package/lib-esm/components/metrics-treemap/index.js.map +1 -1
- package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js +2 -0
- package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js.map +1 -0
- package/lib-esm/components/metrics-treemap/metrics-treemap.js +58 -39
- package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
- package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +55 -25
- package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js +65 -0
- package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js.map +1 -0
- package/lib-esm/hooks/metrics-display-type.js +57 -24
- package/lib-esm/hooks/metrics-display-type.js.map +1 -1
- package/lib-esm/ui/icon/icon.js +1 -0
- package/lib-esm/ui/icon/icon.js.map +1 -1
- package/package.json +7 -7
- package/src/assets/icons/check.svg +4 -0
- package/src/assets/icons.svg +70 -67
- package/src/assets/icons.svg.jsx +12 -12
- package/src/components/bundle-assets/bundle-assets.jsx +76 -20
- package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +5 -4
- package/src/components/bundle-modules/bundle-modules.tsx +76 -16
- package/src/components/bundle-packages/bundle-packages.jsx +71 -16
- package/src/components/metrics-display-selector/metrics-display-selector.module.css +15 -7
- package/src/components/metrics-display-selector/metrics-display-selector.stories.tsx +33 -0
- package/src/components/metrics-display-selector/metrics-display-selector.tsx +83 -25
- package/src/components/metrics-treemap/__tests__/metrics-treemap.utils.ts +143 -0
- package/src/components/metrics-treemap/index.ts +1 -0
- package/src/components/metrics-treemap/metrics-treemap.constants.ts +43 -0
- package/src/components/metrics-treemap/metrics-treemap.module.css +55 -25
- package/src/components/metrics-treemap/metrics-treemap.stories.tsx +44 -10
- package/src/components/metrics-treemap/metrics-treemap.tsx +247 -84
- package/src/components/metrics-treemap/metrics-treemap.utils.ts +107 -0
- package/src/hooks/metrics-display-type.ts +87 -29
- package/src/ui/icon/icon.tsx +1 -0
- package/types/components/metrics-display-selector/metrics-display-selector.d.ts +3 -1
- package/types/components/metrics-treemap/index.d.ts +1 -0
- package/types/components/metrics-treemap/metrics-treemap.constants.d.ts +15 -0
- package/types/components/metrics-treemap/metrics-treemap.d.ts +7 -5
- package/types/components/metrics-treemap/metrics-treemap.utils.d.ts +4 -0
- package/types/hooks/metrics-display-type.d.ts +8 -2
- package/types/ui/icon/icon.d.ts +2 -0
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
import 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
|
|
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 =
|
|
26
|
+
const PADDING_OUTER = 2;
|
|
16
27
|
const PADDING_INNER = 2;
|
|
17
|
-
|
|
18
|
-
type TileSizeDisplay = 'minimal' | 'small' | 'default';
|
|
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,12 +97,21 @@ const TileTooltipContent = (props: TileTooltipContentProps) => {
|
|
|
73
97
|
};
|
|
74
98
|
|
|
75
99
|
interface TileContentProps {
|
|
76
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Tile id - metric label or basename for grouped tiles
|
|
102
|
+
*/
|
|
77
103
|
item: ReportMetricRow;
|
|
104
|
+
/**
|
|
105
|
+
* The estimated size of the tile
|
|
106
|
+
*/
|
|
107
|
+
sizeDisplay: 'minimal' | 'small' | 'default';
|
|
108
|
+
/**
|
|
109
|
+
* Metric run info
|
|
110
|
+
*/
|
|
78
111
|
runInfo: MetricRunInfo;
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
const TileContent = forwardRef((props: TileContentProps, ref:
|
|
114
|
+
const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement>) => {
|
|
82
115
|
const { sizeDisplay, item, runInfo } = props;
|
|
83
116
|
|
|
84
117
|
if (sizeDisplay === 'minimal') {
|
|
@@ -87,12 +120,12 @@ const TileContent = forwardRef((props: TileContentProps, ref: React.Ref<HTMLDivE
|
|
|
87
120
|
|
|
88
121
|
return (
|
|
89
122
|
<div className={css.tileContent} ref={ref}>
|
|
90
|
-
<FileName as="p" className={css.
|
|
123
|
+
<FileName as="p" className={css.tileContentLabel} name={item.label} />
|
|
91
124
|
{sizeDisplay !== 'small' && (
|
|
92
|
-
<p className={css.
|
|
93
|
-
<span className={css.
|
|
125
|
+
<p className={css.tileContentValue}>
|
|
126
|
+
<span className={css.tileContentMetric}>{runInfo.displayValue}</span>
|
|
94
127
|
<Delta
|
|
95
|
-
className={css.
|
|
128
|
+
className={css.tileContentDelta}
|
|
96
129
|
displayValue={runInfo.displayDeltaPercentage}
|
|
97
130
|
deltaType={runInfo.deltaType}
|
|
98
131
|
/>
|
|
@@ -142,25 +175,12 @@ const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject
|
|
|
142
175
|
);
|
|
143
176
|
};
|
|
144
177
|
|
|
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
178
|
interface TileProps {
|
|
159
179
|
left: number;
|
|
160
180
|
top: number;
|
|
161
181
|
width: number;
|
|
162
182
|
height: number;
|
|
163
|
-
data:
|
|
183
|
+
data: TreeLeaf;
|
|
164
184
|
onClick?: (id: string) => void;
|
|
165
185
|
}
|
|
166
186
|
|
|
@@ -171,7 +191,13 @@ const Tile = (props: TileProps) => {
|
|
|
171
191
|
const runInfo = item.runs?.[0] as MetricRunInfo;
|
|
172
192
|
|
|
173
193
|
const sizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
|
|
174
|
-
const handleOnClick = useCallback(
|
|
194
|
+
const handleOnClick: MouseEventHandler<HTMLButtonElement> = useCallback(
|
|
195
|
+
(event) => {
|
|
196
|
+
event.stopPropagation();
|
|
197
|
+
onClick?.(item.key);
|
|
198
|
+
},
|
|
199
|
+
[item.key, onClick],
|
|
200
|
+
);
|
|
175
201
|
|
|
176
202
|
const contentRef = useRef<HTMLButtonElement>(null);
|
|
177
203
|
const hover = useHoverDirty(contentRef);
|
|
@@ -179,7 +205,8 @@ const Tile = (props: TileProps) => {
|
|
|
179
205
|
const className = cx(
|
|
180
206
|
css.tile,
|
|
181
207
|
css[`tile-${runInfo.deltaType}`],
|
|
182
|
-
sizeDisplay === 'small' && css.
|
|
208
|
+
sizeDisplay === 'small' && css.tileSizeSmall,
|
|
209
|
+
sizeDisplay === 'default' && css.tileSizeDefault,
|
|
183
210
|
left === 0 && css.tileFirstCol,
|
|
184
211
|
);
|
|
185
212
|
|
|
@@ -203,96 +230,232 @@ const Tile = (props: TileProps) => {
|
|
|
203
230
|
)}
|
|
204
231
|
</button>
|
|
205
232
|
);
|
|
233
|
+
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
interface TileGroupProps extends ComponentProps<'div'> {
|
|
237
|
+
/**
|
|
238
|
+
* Node title
|
|
239
|
+
*/
|
|
240
|
+
title?: string;
|
|
241
|
+
/**
|
|
242
|
+
* Node id
|
|
243
|
+
*/
|
|
244
|
+
id: string;
|
|
245
|
+
/**
|
|
246
|
+
* Node children
|
|
247
|
+
*/
|
|
248
|
+
childNodes: HierarchyRectangularNode<TreeNode>['children'];
|
|
249
|
+
/**
|
|
250
|
+
* On click tile handler
|
|
251
|
+
*/
|
|
252
|
+
onItemClick: TileProps['onClick'];
|
|
253
|
+
/**
|
|
254
|
+
* On click group title
|
|
255
|
+
*/
|
|
256
|
+
onGroupClick?: (groupId: string) => void;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Node rectangle dimensions
|
|
260
|
+
*/
|
|
261
|
+
width?: number;
|
|
262
|
+
height?: number;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Node rectangle relative coordinates (relative to parent)
|
|
266
|
+
*/
|
|
267
|
+
left: number;
|
|
268
|
+
top: number;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Node rectangle absolute coordinates (relative to canvas)
|
|
272
|
+
*/
|
|
273
|
+
absoluteLeft: number;
|
|
274
|
+
absoluteTop: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const TileGroup = (props: TileGroupProps) => {
|
|
278
|
+
const {
|
|
279
|
+
title = '',
|
|
280
|
+
id,
|
|
281
|
+
childNodes,
|
|
282
|
+
onItemClick,
|
|
283
|
+
onGroupClick,
|
|
284
|
+
left,
|
|
285
|
+
top,
|
|
286
|
+
width,
|
|
287
|
+
height,
|
|
288
|
+
absoluteLeft,
|
|
289
|
+
absoluteTop,
|
|
290
|
+
} = props;
|
|
291
|
+
|
|
292
|
+
const onClick: MouseEventHandler<HTMLDivElement> = useCallback(
|
|
293
|
+
(event) => {
|
|
294
|
+
onGroupClick?.(id);
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
},
|
|
297
|
+
[onGroupClick, id],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const displaySize = useMemo(
|
|
301
|
+
() => resolveTileGroupSizeDisplay(width || 0, height || 0),
|
|
302
|
+
[width, height],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (title && displaySize === 'minimal') {
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
onClick={onClick}
|
|
309
|
+
role="button"
|
|
310
|
+
aria-label="View children entries"
|
|
311
|
+
className={css.tileGroup}
|
|
312
|
+
style={{ left, top, width, height }}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (title && displaySize === 'small') {
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
onClick={onClick}
|
|
321
|
+
role="button"
|
|
322
|
+
aria-label="View children entries"
|
|
323
|
+
style={{ left, top, width, height }}
|
|
324
|
+
className={css.tileGroup}
|
|
325
|
+
>
|
|
326
|
+
<div className={css.tileGroupTitle}>{title}</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
onClick={onClick}
|
|
334
|
+
role="button"
|
|
335
|
+
aria-label="View children entries"
|
|
336
|
+
className={cx(css.tileGroup, css.tileGroupSizeDefault)}
|
|
337
|
+
style={{ left, top, width, height }}
|
|
338
|
+
>
|
|
339
|
+
{title && <div className={css.tileGroupTitle}>{title}</div>}
|
|
340
|
+
{childNodes?.map((childNode) => {
|
|
341
|
+
if ('children' in childNode) {
|
|
342
|
+
return (
|
|
343
|
+
<TileGroup
|
|
344
|
+
title={childNode.data.label}
|
|
345
|
+
id={childNode.data.id}
|
|
346
|
+
childNodes={childNode.children}
|
|
347
|
+
left={childNode.x0 - absoluteLeft}
|
|
348
|
+
top={childNode.y0 - absoluteTop}
|
|
349
|
+
absoluteLeft={childNode.x0}
|
|
350
|
+
absoluteTop={childNode.y0}
|
|
351
|
+
width={childNode.x1 - childNode.x0}
|
|
352
|
+
height={childNode.y1 - childNode.y0}
|
|
353
|
+
onItemClick={onItemClick}
|
|
354
|
+
onGroupClick={onGroupClick}
|
|
355
|
+
key={childNode.data.id}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<Tile
|
|
362
|
+
data={childNode.data as TreeLeaf}
|
|
363
|
+
left={childNode.x0 - absoluteLeft}
|
|
364
|
+
top={childNode.y0 - absoluteTop}
|
|
365
|
+
width={childNode.x1 - childNode.x0}
|
|
366
|
+
height={childNode.y1 - childNode.y0}
|
|
367
|
+
onClick={onItemClick}
|
|
368
|
+
key={childNode.data.id}
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
})}
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
206
374
|
};
|
|
207
375
|
|
|
208
376
|
interface UseMetricsTreemapHierarchyParams {
|
|
209
|
-
|
|
377
|
+
treeNodes: Tree;
|
|
210
378
|
width: number;
|
|
211
379
|
height: number;
|
|
380
|
+
nested: boolean;
|
|
212
381
|
}
|
|
213
382
|
|
|
214
383
|
function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
|
|
215
|
-
const {
|
|
384
|
+
const { treeNodes, width, height, nested } = params;
|
|
216
385
|
|
|
217
386
|
const treemapHierarchy = useMemo(() => {
|
|
218
|
-
const
|
|
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
|
-
};
|
|
387
|
+
const customHierarchy = hierarchy<Tree>(treeNodes);
|
|
239
388
|
|
|
240
|
-
const customHierarchy = hierarchy<RootTree>(treemapData);
|
|
241
389
|
customHierarchy.sum((node) => node.value);
|
|
390
|
+
// sort by value descending for all cases
|
|
242
391
|
// @ts-expect-error
|
|
243
392
|
customHierarchy.sort((a, b) => b.value - a.value);
|
|
244
393
|
|
|
245
394
|
return customHierarchy;
|
|
246
|
-
}, [
|
|
395
|
+
}, [treeNodes]);
|
|
247
396
|
|
|
248
|
-
const
|
|
249
|
-
|
|
397
|
+
const treemapNodes = useMemo(() => {
|
|
398
|
+
let createTremapLayout = treemap()
|
|
250
399
|
.size([width, height])
|
|
251
400
|
.tile(treemapSquarify.ratio(SQUARIFY_RATIO))
|
|
252
401
|
.paddingOuter(PADDING_OUTER)
|
|
253
402
|
.paddingInner(PADDING_INNER);
|
|
254
|
-
// @ts-expect-error
|
|
255
|
-
const tremapLayout = createTremapLayout(treemapHierarchy);
|
|
256
403
|
|
|
257
|
-
|
|
404
|
+
// Add padding top for group title
|
|
405
|
+
if (nested) {
|
|
406
|
+
createTremapLayout = createTremapLayout.paddingTop(GROUPED_PADDING_TOP);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// @ts-expect-error
|
|
410
|
+
return createTremapLayout(treemapHierarchy) as HierarchyRectangularNode<TreeNode>;
|
|
258
411
|
}, [height, width, treemapHierarchy]);
|
|
259
412
|
|
|
260
|
-
return
|
|
413
|
+
return treemapNodes;
|
|
261
414
|
}
|
|
262
415
|
|
|
263
416
|
interface MetricsTreemapProps {
|
|
264
|
-
|
|
265
|
-
emptyMessage?:
|
|
417
|
+
treeNodes: Tree;
|
|
418
|
+
emptyMessage?: ReactNode;
|
|
419
|
+
nested?: boolean;
|
|
266
420
|
onItemClick?: (entryId: string) => void;
|
|
421
|
+
onGroupClick?: (entryId: string) => void;
|
|
267
422
|
}
|
|
268
423
|
|
|
269
|
-
export const MetricsTreemap = (props: MetricsTreemapProps &
|
|
270
|
-
const {
|
|
424
|
+
export const MetricsTreemap = (props: MetricsTreemapProps & ComponentProps<'div'>) => {
|
|
425
|
+
const {
|
|
426
|
+
className = '',
|
|
427
|
+
treeNodes,
|
|
428
|
+
onItemClick,
|
|
429
|
+
onGroupClick,
|
|
430
|
+
emptyMessage = 'No data',
|
|
431
|
+
nested = false,
|
|
432
|
+
...restProps
|
|
433
|
+
} = props;
|
|
271
434
|
|
|
272
435
|
const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
|
|
273
|
-
const
|
|
436
|
+
const rootNode = useMetricsTreemapHierarchy({ treeNodes, width, height, nested });
|
|
274
437
|
|
|
275
438
|
return (
|
|
276
|
-
<div
|
|
277
|
-
{
|
|
439
|
+
<div
|
|
440
|
+
className={cx(css.root, nested && css.nested, className)}
|
|
441
|
+
{...restProps}
|
|
442
|
+
ref={containerRef}
|
|
443
|
+
>
|
|
444
|
+
{rootNode.children && rootNode.children.length > 0 ? (
|
|
278
445
|
<div className={css.canvas}>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
key={tile.id}
|
|
293
|
-
/>
|
|
294
|
-
);
|
|
295
|
-
})}
|
|
446
|
+
<TileGroup
|
|
447
|
+
title={nested ? rootNode.data.label : undefined}
|
|
448
|
+
id={rootNode.data.id}
|
|
449
|
+
childNodes={rootNode.children}
|
|
450
|
+
onItemClick={onItemClick}
|
|
451
|
+
onGroupClick={onGroupClick}
|
|
452
|
+
left={rootNode.x0}
|
|
453
|
+
top={rootNode.y0}
|
|
454
|
+
absoluteLeft={rootNode.x0}
|
|
455
|
+
absoluteTop={rootNode.y0}
|
|
456
|
+
width={rootNode.x1 - rootNode.x0}
|
|
457
|
+
height={rootNode.y1 - rootNode.y0}
|
|
458
|
+
/>
|
|
296
459
|
</div>
|
|
297
460
|
) : (
|
|
298
461
|
<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
|
+
}
|
|
@@ -4,53 +4,111 @@ import { StringParam, useQueryParams } from 'use-query-params';
|
|
|
4
4
|
|
|
5
5
|
import { MetricsDisplayType } from '../constants';
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const DEFAULT_VALUE = MetricsDisplayType.TABLE;
|
|
8
|
+
const DEFAULT_GROUP_BY = '';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Display type query param name
|
|
11
|
+
const QUERY_PARAM_VALUE = 'dt';
|
|
12
|
+
// Display type group by param name
|
|
13
|
+
const QUERY_PARAM_GROUP_BY = 'dtg';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
type UseMetricsDisplayType = [
|
|
16
|
+
{ value: MetricsDisplayType; groupBy?: string },
|
|
17
|
+
(value: MetricsDisplayType, newGroupBy?: string) => void,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const useMetricsDisplayType = (
|
|
21
|
+
groups?: Partial<Record<MetricsDisplayType, Array<string>>>,
|
|
22
|
+
): UseMetricsDisplayType => {
|
|
23
|
+
const [queryParams, setQueryParams] = useQueryParams({
|
|
24
|
+
[QUERY_PARAM_VALUE]: StringParam,
|
|
25
|
+
[QUERY_PARAM_GROUP_BY]: StringParam,
|
|
15
26
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
|
|
28
|
+
const { [QUERY_PARAM_VALUE]: queryParamValue, [QUERY_PARAM_GROUP_BY]: queryParamGroupBy } =
|
|
29
|
+
queryParams;
|
|
30
|
+
|
|
31
|
+
const [storageValue, setStorageValue] = useLocalStorage(QUERY_PARAM_VALUE, DEFAULT_VALUE);
|
|
32
|
+
const [storageGroupBy, setStorageGroupBy] = useLocalStorage(
|
|
33
|
+
QUERY_PARAM_GROUP_BY,
|
|
34
|
+
DEFAULT_GROUP_BY,
|
|
19
35
|
);
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Normalize display type value
|
|
39
|
+
*/
|
|
40
|
+
const value = useMemo(() => {
|
|
41
|
+
const resolvedValue = queryParamValue || storageValue;
|
|
42
|
+
|
|
43
|
+
if (!resolvedValue) {
|
|
44
|
+
return DEFAULT_VALUE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!Object.values(MetricsDisplayType).includes(resolvedValue as MetricsDisplayType)) {
|
|
48
|
+
return DEFAULT_VALUE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return resolvedValue as MetricsDisplayType;
|
|
52
|
+
}, [queryParamValue, storageValue]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalize display type group by
|
|
56
|
+
*/
|
|
57
|
+
const groupBy = useMemo(() => {
|
|
58
|
+
const resolvedValue = queryParamGroupBy || storageGroupBy;
|
|
23
59
|
|
|
24
|
-
if (!
|
|
25
|
-
return
|
|
60
|
+
if (!resolvedValue) {
|
|
61
|
+
return DEFAULT_GROUP_BY;
|
|
26
62
|
}
|
|
27
63
|
|
|
28
|
-
if (!
|
|
29
|
-
return
|
|
64
|
+
if (!groups?.[value]?.includes(resolvedValue as MetricsDisplayType)) {
|
|
65
|
+
return DEFAULT_GROUP_BY;
|
|
30
66
|
}
|
|
31
67
|
|
|
32
|
-
return
|
|
33
|
-
}, [
|
|
68
|
+
return resolvedValue;
|
|
69
|
+
}, [queryParamGroupBy, storageGroupBy]);
|
|
34
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Synchronize query params when values are different
|
|
73
|
+
*/
|
|
35
74
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
setQueryParams({ [PARAM_NAME]: displayType });
|
|
75
|
+
if (value !== queryParamValue || groupBy !== queryParamGroupBy) {
|
|
76
|
+
setQueryParams({ [QUERY_PARAM_VALUE]: value, [QUERY_PARAM_GROUP_BY]: groupBy });
|
|
39
77
|
}
|
|
78
|
+
}, [value, queryParamValue, groupBy, queryParamGroupBy, setQueryParams]);
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Synchronize storeage when values are different
|
|
82
|
+
*/
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (value !== storageValue) {
|
|
85
|
+
setStorageValue(value);
|
|
86
|
+
}
|
|
87
|
+
if (groupBy !== storageValue) {
|
|
88
|
+
setStorageGroupBy(groupBy);
|
|
44
89
|
}
|
|
45
|
-
}, [
|
|
90
|
+
}, [
|
|
91
|
+
value,
|
|
92
|
+
storageValue,
|
|
93
|
+
groupBy,
|
|
94
|
+
storageGroupBy,
|
|
95
|
+
setQueryParams,
|
|
96
|
+
setStorageValue,
|
|
97
|
+
setStorageGroupBy,
|
|
98
|
+
]);
|
|
46
99
|
|
|
47
100
|
const setDisplayType = useCallback(
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
101
|
+
(newValue: MetricsDisplayType, newGroupBy: string = DEFAULT_GROUP_BY) => {
|
|
102
|
+
setStorageValue(newValue);
|
|
103
|
+
setStorageGroupBy(newGroupBy);
|
|
104
|
+
|
|
105
|
+
setQueryParams({
|
|
106
|
+
[QUERY_PARAM_VALUE]: newValue,
|
|
107
|
+
[QUERY_PARAM_GROUP_BY]: newGroupBy,
|
|
108
|
+
});
|
|
51
109
|
},
|
|
52
|
-
[
|
|
110
|
+
[setStorageValue, setQueryParams],
|
|
53
111
|
);
|
|
54
112
|
|
|
55
|
-
return [
|
|
113
|
+
return [{ value, groupBy }, setDisplayType];
|
|
56
114
|
};
|