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