@bundle-stats/ui 4.12.0 → 4.13.0-beta.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 (174) hide show
  1. package/lib/app/app.js +3 -7
  2. package/lib/app/app.js.map +1 -1
  3. package/lib/assets/icons.svg +26 -20
  4. package/lib/assets/icons.svg.js +27 -23
  5. package/lib/assets/icons.svg.js.map +1 -1
  6. package/lib/components/bundle-assets/bundle-assets.js +11 -3
  7. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  8. package/lib/components/bundle-assets/index.js +3 -4
  9. package/lib/components/bundle-assets/index.js.map +1 -1
  10. package/lib/components/bundle-assets-totals/bundle-assets-totals.js +77 -0
  11. package/lib/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -0
  12. package/lib/components/bundle-assets-totals/bundle-assets-totals.module.css +7 -0
  13. package/lib/{ui/horizontal-bar-chart → components/bundle-assets-totals}/index.js +1 -1
  14. package/lib/components/bundle-assets-totals/index.js.map +1 -0
  15. package/lib/components/bundle-modules/bundle-modules.js +9 -2
  16. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  17. package/lib/components/bundle-modules/index.js +3 -4
  18. package/lib/components/bundle-modules/index.js.map +1 -1
  19. package/lib/components/bundle-packages/bundle-packages.js +10 -2
  20. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  21. package/lib/components/bundle-packages/index.js +3 -4
  22. package/lib/components/bundle-packages/index.js.map +1 -1
  23. package/lib/components/index.js +3 -5
  24. package/lib/components/index.js.map +1 -1
  25. package/lib/components/{bundle-assets-totals-table → metrics-display-selector}/index.js +1 -1
  26. package/lib/components/metrics-display-selector/index.js.map +1 -0
  27. package/lib/components/metrics-display-selector/metrics-display-selector.js +27 -0
  28. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -0
  29. package/lib/components/metrics-display-selector/metrics-display-selector.module.css +10 -0
  30. package/lib/components/{bundle-assets-totals-chart-bars → metrics-treemap}/index.js +1 -1
  31. package/lib/components/metrics-treemap/index.js.map +1 -0
  32. package/lib/components/metrics-treemap/metrics-treemap.js +148 -0
  33. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -0
  34. package/lib/components/metrics-treemap/metrics-treemap.module.css +168 -0
  35. package/lib/constants.js +6 -1
  36. package/lib/constants.js.map +1 -1
  37. package/lib/hooks/metrics-display-type.js +40 -0
  38. package/lib/hooks/metrics-display-type.js.map +1 -0
  39. package/lib/hooks/show-entry.js +13 -0
  40. package/lib/hooks/show-entry.js.map +1 -0
  41. package/lib/ui/icon/icon.js +2 -0
  42. package/lib/ui/icon/icon.js.map +1 -1
  43. package/lib/ui/index.js +1 -3
  44. package/lib/ui/index.js.map +1 -1
  45. package/lib-esm/app/app.js +3 -7
  46. package/lib-esm/app/app.js.map +1 -1
  47. package/lib-esm/assets/icons.svg +26 -20
  48. package/lib-esm/assets/icons.svg.js +27 -23
  49. package/lib-esm/assets/icons.svg.js.map +1 -1
  50. package/lib-esm/components/bundle-assets/bundle-assets.js +11 -3
  51. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  52. package/lib-esm/components/bundle-assets/index.js +4 -5
  53. package/lib-esm/components/bundle-assets/index.js.map +1 -1
  54. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js +47 -0
  55. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.js.map +1 -0
  56. package/lib-esm/components/bundle-assets-totals/bundle-assets-totals.module.css +7 -0
  57. package/lib-esm/components/bundle-assets-totals/index.js +2 -0
  58. package/lib-esm/components/bundle-assets-totals/index.js.map +1 -0
  59. package/lib-esm/components/bundle-modules/bundle-modules.js +9 -2
  60. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  61. package/lib-esm/components/bundle-modules/index.js +3 -4
  62. package/lib-esm/components/bundle-modules/index.js.map +1 -1
  63. package/lib-esm/components/bundle-packages/bundle-packages.js +10 -2
  64. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  65. package/lib-esm/components/bundle-packages/index.js +4 -5
  66. package/lib-esm/components/bundle-packages/index.js.map +1 -1
  67. package/lib-esm/components/index.js +1 -2
  68. package/lib-esm/components/index.js.map +1 -1
  69. package/lib-esm/components/metrics-display-selector/index.js +2 -0
  70. package/lib-esm/components/metrics-display-selector/index.js.map +1 -0
  71. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +20 -0
  72. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -0
  73. package/lib-esm/components/metrics-display-selector/metrics-display-selector.module.css +10 -0
  74. package/lib-esm/components/metrics-treemap/index.js +2 -0
  75. package/lib-esm/components/metrics-treemap/index.js.map +1 -0
  76. package/lib-esm/components/metrics-treemap/metrics-treemap.js +119 -0
  77. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -0
  78. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +168 -0
  79. package/lib-esm/constants.js +5 -0
  80. package/lib-esm/constants.js.map +1 -1
  81. package/lib-esm/hooks/metrics-display-type.js +36 -0
  82. package/lib-esm/hooks/metrics-display-type.js.map +1 -0
  83. package/lib-esm/hooks/show-entry.js +9 -0
  84. package/lib-esm/hooks/show-entry.js.map +1 -0
  85. package/lib-esm/ui/icon/icon.js +2 -0
  86. package/lib-esm/ui/icon/icon.js.map +1 -1
  87. package/lib-esm/ui/index.js +0 -1
  88. package/lib-esm/ui/index.js.map +1 -1
  89. package/package.json +11 -6
  90. package/src/app/app.jsx +4 -8
  91. package/src/assets/icons/table.svg +3 -0
  92. package/src/assets/icons/treemap.svg +4 -0
  93. package/src/assets/icons.svg +26 -20
  94. package/src/assets/icons.svg.jsx +91 -55
  95. package/src/components/bundle-assets/bundle-assets.jsx +25 -11
  96. package/src/components/bundle-assets/index.jsx +4 -4
  97. package/src/components/bundle-assets-totals/bundle-assets-totals.jsx +94 -0
  98. package/src/components/bundle-assets-totals/bundle-assets-totals.module.css +7 -0
  99. package/src/components/bundle-assets-totals/index.js +1 -0
  100. package/src/components/bundle-modules/bundle-modules.tsx +26 -12
  101. package/src/components/bundle-modules/index.tsx +4 -3
  102. package/src/components/bundle-packages/bundle-packages.jsx +24 -10
  103. package/src/components/bundle-packages/index.jsx +4 -4
  104. package/src/components/index.js +1 -2
  105. package/src/components/metrics-display-selector/index.tsx +1 -0
  106. package/src/components/metrics-display-selector/metrics-display-selector.module.css +10 -0
  107. package/src/components/metrics-display-selector/metrics-display-selector.tsx +51 -0
  108. package/src/components/metrics-treemap/index.ts +1 -0
  109. package/src/components/metrics-treemap/metrics-treemap.module.css +168 -0
  110. package/src/components/metrics-treemap/metrics-treemap.stories.tsx +23 -0
  111. package/src/components/metrics-treemap/metrics-treemap.tsx +271 -0
  112. package/src/{constants.js → constants.ts} +5 -0
  113. package/src/hooks/metrics-display-type.ts +56 -0
  114. package/src/hooks/show-entry.ts +17 -0
  115. package/src/types.d.ts +2 -0
  116. package/src/ui/icon/icon.tsx +2 -0
  117. package/src/ui/index.js +0 -1
  118. package/types/components/bundle-assets/bundle-assets.d.ts +2 -1
  119. package/types/components/{bundle-assets-totals-table/bundle-assets-totals-table.d.ts → bundle-assets-totals/bundle-assets-totals.d.ts} +2 -2
  120. package/types/components/bundle-assets-totals/index.d.ts +1 -0
  121. package/types/components/bundle-modules/bundle-modules.d.ts +2 -1
  122. package/types/components/bundle-modules/index.d.ts +1 -1
  123. package/types/components/bundle-packages/bundle-packages.d.ts +1 -0
  124. package/types/components/index.d.ts +1 -2
  125. package/types/components/metrics-display-selector/index.d.ts +1 -0
  126. package/types/components/metrics-display-selector/metrics-display-selector.d.ts +9 -0
  127. package/types/components/metrics-treemap/index.d.ts +1 -0
  128. package/types/components/metrics-treemap/metrics-treemap.d.ts +9 -0
  129. package/types/constants.d.ts +19 -15
  130. package/types/hooks/metrics-display-type.d.ts +4 -0
  131. package/types/hooks/show-entry.d.ts +10 -0
  132. package/types/ui/icon/icon.d.ts +4 -0
  133. package/types/ui/index.d.ts +0 -1
  134. package/lib/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.js +0 -95
  135. package/lib/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.js.map +0 -1
  136. package/lib/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.module.css +0 -42
  137. package/lib/components/bundle-assets-totals-chart-bars/index.js.map +0 -1
  138. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.js +0 -57
  139. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.js.map +0 -1
  140. package/lib/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +0 -3
  141. package/lib/components/bundle-assets-totals-table/index.js.map +0 -1
  142. package/lib/ui/horizontal-bar-chart/horizontal-bar-chart.js +0 -102
  143. package/lib/ui/horizontal-bar-chart/horizontal-bar-chart.js.map +0 -1
  144. package/lib/ui/horizontal-bar-chart/horizontal-bar-chart.module.css +0 -42
  145. package/lib/ui/horizontal-bar-chart/index.js.map +0 -1
  146. package/lib-esm/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.js +0 -66
  147. package/lib-esm/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.js.map +0 -1
  148. package/lib-esm/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.module.css +0 -42
  149. package/lib-esm/components/bundle-assets-totals-chart-bars/index.js +0 -2
  150. package/lib-esm/components/bundle-assets-totals-chart-bars/index.js.map +0 -1
  151. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.js +0 -27
  152. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.js.map +0 -1
  153. package/lib-esm/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +0 -3
  154. package/lib-esm/components/bundle-assets-totals-table/index.js +0 -2
  155. package/lib-esm/components/bundle-assets-totals-table/index.js.map +0 -1
  156. package/lib-esm/ui/horizontal-bar-chart/horizontal-bar-chart.js +0 -96
  157. package/lib-esm/ui/horizontal-bar-chart/horizontal-bar-chart.js.map +0 -1
  158. package/lib-esm/ui/horizontal-bar-chart/horizontal-bar-chart.module.css +0 -42
  159. package/lib-esm/ui/horizontal-bar-chart/index.js +0 -2
  160. package/lib-esm/ui/horizontal-bar-chart/index.js.map +0 -1
  161. package/src/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.jsx +0 -106
  162. package/src/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.module.css +0 -42
  163. package/src/components/bundle-assets-totals-chart-bars/index.js +0 -1
  164. package/src/components/bundle-assets-totals-table/bundle-assets-totals-table.jsx +0 -55
  165. package/src/components/bundle-assets-totals-table/bundle-assets-totals-table.module.css +0 -3
  166. package/src/components/bundle-assets-totals-table/index.js +0 -1
  167. package/src/ui/horizontal-bar-chart/horizontal-bar-chart.jsx +0 -156
  168. package/src/ui/horizontal-bar-chart/horizontal-bar-chart.module.css +0 -42
  169. package/src/ui/horizontal-bar-chart/index.js +0 -1
  170. package/types/components/bundle-assets-totals-chart-bars/bundle-assets-totals-chart-bars.d.ts +0 -22
  171. package/types/components/bundle-assets-totals-chart-bars/index.d.ts +0 -1
  172. package/types/components/bundle-assets-totals-table/index.d.ts +0 -1
  173. package/types/ui/horizontal-bar-chart/horizontal-bar-chart.d.ts +0 -23
  174. package/types/ui/horizontal-bar-chart/index.d.ts +0 -1
@@ -18,6 +18,10 @@ import { MetricsTableTitle } from '../metrics-table-title';
18
18
  import { PackageInfo } from '../package-info';
19
19
  import { SEARCH_PLACEHOLDER } from './bundle-packages.i18n';
20
20
  import css from './bundle-packages.module.css';
21
+ import { MetricsDisplaySelector } from '../metrics-display-selector';
22
+ import { MetricsDisplayType } from '../../constants';
23
+ import { MetricsTreemap } from '../metrics-treemap';
24
+ import { useMetricsDisplayType } from '../../hooks/metrics-display-type';
21
25
 
22
26
  const getDropdownFilters = ({ compareMode, filters }) => ({
23
27
  [PACKAGE_FILTERS.CHANGED]: {
@@ -126,10 +130,13 @@ export const BundlePackages = (props) => {
126
130
  entryId,
127
131
  allItems,
128
132
  hideEntryInfo,
133
+ showEntryInfo,
129
134
  } = props;
130
135
 
131
136
  const jobLabels = jobs?.map((job) => job?.label);
132
137
 
138
+ const [displayType, setDisplayType] = useMetricsDisplayType();
139
+
133
140
  const dropdownFilters = useMemo(
134
141
  () => getDropdownFilters({ compareMode: jobs?.length > 1, filters }),
135
142
  [jobs, filters],
@@ -187,6 +194,7 @@ export const BundlePackages = (props) => {
187
194
  className={css.toolbar}
188
195
  renderActions={({ actionClassName }) => (
189
196
  <FlexStack space="xxsmall" className={cx(css.dropdown, actionClassName)}>
197
+ <MetricsDisplaySelector onSelect={setDisplayType} value={displayType} />
190
198
  <MetricsTableOptions
191
199
  handleViewAll={resetAllFilters}
192
200
  handleResetFilters={resetFilters}
@@ -210,16 +218,21 @@ export const BundlePackages = (props) => {
210
218
  </FlexStack>
211
219
  </Toolbar>
212
220
  <main>
213
- <MetricsTable
214
- runs={jobs}
215
- items={items}
216
- emptyMessage={emptyMessage}
217
- renderRowHeader={renderRowHeader}
218
- showHeaderSum
219
- title={metricsTableTitle}
220
- sort={sort}
221
- updateSort={updateSort}
222
- />
221
+ {displayType === MetricsDisplayType.TABLE && (
222
+ <MetricsTable
223
+ runs={jobs}
224
+ items={items}
225
+ emptyMessage={emptyMessage}
226
+ renderRowHeader={renderRowHeader}
227
+ showHeaderSum
228
+ title={metricsTableTitle}
229
+ sort={sort}
230
+ updateSort={updateSort}
231
+ />
232
+ )}
233
+ {displayType === MetricsDisplayType.TREEMAP && (
234
+ <MetricsTreemap emptyMessage={emptyMessage} items={items} onItemClick={showEntryInfo} />
235
+ )}
223
236
  </main>
224
237
  </section>
225
238
  {entryItem && (
@@ -283,4 +296,5 @@ BundlePackages.propTypes = {
283
296
  customComponentLink: PropTypes.elementType,
284
297
  entryId: PropTypes.string,
285
298
  hideEntryInfo: PropTypes.func.isRequired,
299
+ showEntryInfo: PropTypes.func.isRequired,
286
300
  };
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { PACKAGE_FILTERS } from '@bundle-stats/utils';
4
4
  import * as webpack from '@bundle-stats/utils/lib-esm/webpack/compare';
@@ -6,6 +6,7 @@ import * as webpack from '@bundle-stats/utils/lib-esm/webpack/compare';
6
6
  import { useRowsFilter } from '../../hooks/rows-filter';
7
7
  import { useRowsSort } from '../../hooks/rows-sort';
8
8
  import { useSearchParams } from '../../hooks/search-params';
9
+ import { useShowEntry } from '../../hooks/show-entry';
9
10
  import { BundlePackages as BundlePackagesComponent } from './bundle-packages';
10
11
  import { getDuplicatePackages, getAddRowDuplicateFlag, getRowFilter, getCustomSort } from './bundle-packages.utils';
11
12
 
@@ -54,9 +55,7 @@ export const BundlePackages = (props) => {
54
55
  getCustomSort,
55
56
  });
56
57
 
57
- const hideEntryInfo = useCallback(() => {
58
- setState({ entryId: '' });
59
- }, [setState]);
58
+ const { hideEntryInfo, showEntryInfo } = useShowEntry({ setState });
60
59
 
61
60
  return (
62
61
  <BundlePackagesComponent
@@ -67,6 +66,7 @@ export const BundlePackages = (props) => {
67
66
  allItems={rows}
68
67
  totalRowCount={totalRowCount}
69
68
  hideEntryInfo={hideEntryInfo}
69
+ showEntryInfo={showEntryInfo}
70
70
  />
71
71
  );
72
72
  };
@@ -1,7 +1,6 @@
1
1
  export { BrowsertimeTable } from './browsertime-table';
2
2
  export { BundleAssets } from './bundle-assets';
3
- export { BundleAssetsTotalsTable } from './bundle-assets-totals-table';
4
- export { BundleAssetsTotalsChartBars } from './bundle-assets-totals-chart-bars';
3
+ export { BundleAssetsTotals } from './bundle-assets-totals';
5
4
  export { BundlePackages } from './bundle-packages';
6
5
  export { BundleModules } from './bundle-modules';
7
6
  export { ComponentLink } from './component-link';
@@ -0,0 +1 @@
1
+ export * from './metrics-display-selector';
@@ -0,0 +1,10 @@
1
+ .button {
2
+ text-decoration: none;
3
+ }
4
+
5
+ .buttonActive,
6
+ .buttonActive:active,
7
+ .buttonActive:focus {
8
+ color: var(--color-primary);
9
+ border-color: var(--color-primary);
10
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import cx from 'classnames';
3
+
4
+ import { MetricsDisplayType } from '../../constants';
5
+ import { FlexStack } from '../../layout/flex-stack';
6
+ import { Button } from '../../ui/button';
7
+ import { Icon } from '../../ui/icon';
8
+ import css from './metrics-display-selector.module.css';
9
+
10
+ interface MetricsDisplaySelectorProps {
11
+ value: MetricsDisplayType;
12
+ onSelect: (newValue: MetricsDisplayType) => void;
13
+ }
14
+
15
+ export const MetricsDisplaySelector = (
16
+ props: MetricsDisplaySelectorProps &
17
+ Omit<React.ComponentProps<typeof FlexStack>, 'onSelect' | 'value'>,
18
+ ) => {
19
+ const { className = '', value, onSelect, ...restProps } = props;
20
+
21
+ return (
22
+ <FlexStack space="xxsmall" as="nav" className={cx(css.root, className)} {...restProps}>
23
+ <Button
24
+ outline
25
+ active={value === MetricsDisplayType.TABLE}
26
+ size="small"
27
+ type="button"
28
+ onClick={() => onSelect(MetricsDisplayType.TABLE)}
29
+ className={css.button}
30
+ >
31
+ <FlexStack space="xxxsmall" alignItems="center">
32
+ <Icon glyph={Icon.ICONS.TABLE} className={css.icon} />
33
+ <span>Table</span>
34
+ </FlexStack>
35
+ </Button>
36
+ <Button
37
+ outline
38
+ active={value === MetricsDisplayType.TREEMAP}
39
+ size="small"
40
+ type="button"
41
+ onClick={() => onSelect(MetricsDisplayType.TREEMAP)}
42
+ className={cx(css.button)}
43
+ >
44
+ <FlexStack space="xxxsmall" alignItems="center">
45
+ <Icon glyph={Icon.ICONS.TREEMAP} className={css.icon} />
46
+ <span>Treemap</span>
47
+ </FlexStack>
48
+ </Button>
49
+ </FlexStack>
50
+ );
51
+ };
@@ -0,0 +1 @@
1
+ export * from './metrics-treemap';
@@ -0,0 +1,168 @@
1
+ .root {
2
+ position: relative;
3
+ aspect-ratio: 16/9;
4
+ background: var(--color-background);
5
+ }
6
+
7
+ .canvas {
8
+ position: absolute;
9
+ left: 0;
10
+ top: 0;
11
+ width: 100%;
12
+ height: 100%;
13
+ fill: var(--color-text);
14
+ font-family: var(--font-family);
15
+ }
16
+
17
+ /** Tile overlay fill background transition */
18
+ .root {
19
+ transition: var(--ui-transition-out);
20
+ transition-property: opacity;
21
+ }
22
+
23
+ .tileOverlay {
24
+ fill: var(--color-outline);
25
+ transition: var(--ui-transition-out);
26
+ transition-property: opacity;
27
+ opacity: 0.4;
28
+ }
29
+
30
+ .root .tile:hover .tileOverlay {
31
+ transition: var(--ui-transition-in);
32
+ opacity: 0.8;
33
+ }
34
+
35
+ /** Tile content */
36
+ .tileContent {
37
+ appearance: none;
38
+ border: 0;
39
+ border-radius: none;
40
+ padding: 0;
41
+ background: none;
42
+
43
+ width: 100%;
44
+ height: 100%;
45
+ padding: var(--space-xxsmall) var(--space-small);
46
+ display: flex;
47
+ align-items: center;
48
+ font-family: var(--font-family-fixed);
49
+ font-size: var(--size-small);
50
+ line-height: var(--line-height-heading);
51
+ text-align: left;
52
+ }
53
+
54
+ .tileContentWrapper {
55
+ width: 100%;
56
+ max-height: 100%;
57
+ display: flex;
58
+ flex-flow: column;
59
+ gap: 4px;
60
+ }
61
+
62
+ .tileContentLabel {
63
+ margin-right: calc(0px - var(--space-xxsmall)); /* take more space if necessary */
64
+ margin-bottom: calc(0px - 2px);
65
+ flex: 1 1 auto;
66
+ max-width: calc(100% + var(--space-xxsmall)); /* take more space if necessary */
67
+ overflow: hidden;
68
+ word-break: break-word;
69
+ }
70
+
71
+ .tileContentValue {
72
+ flex: 0 0 auto;
73
+ max-width: calc(100% + var(--space-xxsmall));
74
+ margin-right: calc(0px - var(--space-xxsmall)); /* take more space if necessary */
75
+ white-space: nowrap;
76
+ overflow: hidden;
77
+ }
78
+
79
+ .tileContentMetric {
80
+ font-weight: bold;
81
+ }
82
+
83
+ .tileContentDelta {
84
+ margin-left: var(--space-xxxsmall);
85
+ }
86
+
87
+ /** Tile size variation */
88
+ .tileSmall .tileContent {
89
+ font-size: var(--size-xsmall);
90
+ }
91
+
92
+ .tileSmall .tileContentLabel {
93
+ margin-top: 0;
94
+ }
95
+
96
+ /** Tile diff variation */
97
+ .tile-NO_CHANGE .tileContent {
98
+ color: var(--color-text-light);
99
+ }
100
+
101
+ .tile-HIGH_NEGATIVE .tileOverlay {
102
+ fill: var(--color-red-100);
103
+ }
104
+
105
+ .tile-NEGATIVE .tileOverlay {
106
+ fill: var(--color-red-50);
107
+ }
108
+
109
+ .tile-LOW_NEGATIVE .tileOverlay {
110
+ fill: var(--color-red-25);
111
+ }
112
+
113
+ .tile-LOW_POSITIVE .tileOverlay {
114
+ fill: var(--color-green-25);
115
+ }
116
+
117
+ .tile-POSITIVE .tileOverlay {
118
+ fill: var(--color-green-50);
119
+ }
120
+
121
+ .tile-HIGH_POSITIVE .tileOverlay {
122
+ fill: var(--color-green-100);
123
+ }
124
+
125
+
126
+ /** empty message */
127
+ .emptyMessage {
128
+ position: absolute;
129
+ left: 0;
130
+ top: 0;
131
+ width: 100%;
132
+ height: 100%;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ }
137
+
138
+ .emptyMessageWrapper {
139
+ max-width: 100%;
140
+ text-align: center;
141
+ }
142
+
143
+ /** tooltip content */
144
+ .tooltip {
145
+ z-index: var(--layer-xxhigh);
146
+ padding: var(--space-xsmall) var(--space-small);
147
+ border-radius: var(--radius-medium);
148
+ width: calc(100vw - 2 * var(--space-small));
149
+ max-width: 340px;
150
+ outline: none;
151
+ filter: drop-shadow(var(--shadow-layer-high));
152
+ will-change: filter;
153
+ background: var(--color-light);
154
+ font-size: var(--size-small);
155
+ }
156
+
157
+ .tooltipArrow {
158
+ border: var(--space-xxsmall) solid var(--color-background);
159
+ border-width: var(--space-xxsmall) var(--space-xxsmall) 0;
160
+ border-left-color: transparent;
161
+ border-right-color: transparent;
162
+ position: absolute;
163
+ top: 100%;
164
+ }
165
+
166
+ .tooltipContentTitle {
167
+ font-size: var(--size-medium);
168
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
4
+ import { action } from '@storybook/addon-actions';
5
+
6
+ // eslint-disable-next-line import/no-relative-packages
7
+ import moduleItemsFixture from '../../../../../fixtures/module-items.json';
8
+ import { MetricsTreemap } from '.';
9
+
10
+ export default {
11
+ title: 'Components/MetricsTreemap',
12
+ component: MetricsTreemap,
13
+ } as Meta;
14
+
15
+ export const Default: StoryObj = {
16
+ render: (props: React.ComponentProps<typeof MetricsTreemap>) => (
17
+ <MetricsTreemap style={{ maxWidth: 'var(--max-width)' }} {...props} />
18
+ ),
19
+ args: {
20
+ items: moduleItemsFixture,
21
+ onItemClick: action('CLICK'),
22
+ },
23
+ };
@@ -0,0 +1,271 @@
1
+ import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
2
+ import cx from 'classnames';
3
+ import { useMeasure } from 'react-use';
4
+ import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
5
+ import { hierarchy, treemap, treemapSquarify } from 'd3';
6
+ import {
7
+ FloatingPortal,
8
+ useClientPoint,
9
+ useFloating,
10
+ useHover,
11
+ useInteractions,
12
+ } from '@floating-ui/react';
13
+ import { arrow, offset } from '@floating-ui/react-dom';
14
+
15
+ import { Stack } from '../../layout/stack';
16
+ import { FileName } from '../../ui/file-name';
17
+ import { Delta } from '../delta';
18
+ import { RunInfo } from '../run-info';
19
+ import css from './metrics-treemap.module.css';
20
+
21
+ const SQUARIFY_RATIO = 1.78;
22
+ const PADDING_OUTER = 0;
23
+ const PADDING_INNER = 1;
24
+
25
+ type TileSizeDisplay = 'minimal' | 'small' | 'default';
26
+
27
+ /**
28
+ * Resolve the tile size using predefined values to avoid
29
+ * computing the size of the content for every tile
30
+ */
31
+ const PADDING_TOP = 4;
32
+ const PADDING_BOTTOM = 2; // substracted to allow longer texts to appear
33
+ const PADDING_LEFT = 16;
34
+ const PADDING_RIGHT = 4; // substracted to allow longer texts to not break
35
+ const VERTICAL_SPACING = 4;
36
+ const LINE_HEIGHT = 16;
37
+ const LINE_HEIGHT_SMALL = 13.3;
38
+
39
+ function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
40
+ if (
41
+ height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
42
+ width > PADDING_LEFT + PADDING_RIGHT + 96
43
+ ) {
44
+ return 'default';
45
+ }
46
+
47
+ if (
48
+ height > PADDING_TOP + PADDING_BOTTOM + LINE_HEIGHT_SMALL * 2 &&
49
+ width > PADDING_LEFT + PADDING_BOTTOM + 48
50
+ ) {
51
+ return 'small';
52
+ }
53
+
54
+ return 'minimal';
55
+ }
56
+
57
+ interface TooltipContentProps {
58
+ data: ReportMetricRow;
59
+ }
60
+
61
+ const TooltipContent = (props: TooltipContentProps) => {
62
+ const { data } = props;
63
+ const currentRun = data.runs[0] as MetricRunInfo;
64
+ const baselineRun = data.runs[data.runs.length - 1] as MetricRunInfoBaseline;
65
+
66
+ return (
67
+ <Stack space="small" className={css.tooltipContent}>
68
+ <h3 className={css.tooltipContentTitle}>
69
+ <FileName as="code" name={data.label} />
70
+ </h3>
71
+ <RunInfo
72
+ current={currentRun.displayValue}
73
+ delta={currentRun.displayDeltaPercentage}
74
+ deltaType={currentRun.deltaType}
75
+ baseline={baselineRun?.displayValue || '0B'}
76
+ />
77
+ </Stack>
78
+ );
79
+ };
80
+
81
+ const TileButton = forwardRef((props: React.ComponentProps<'button'>, ref) => (
82
+ // @ts-expect-error
83
+ <button type="button" xmlns="http://www.w3.org/1999/xhtml" ref={ref} {...props} />
84
+ ));
85
+
86
+ interface TreeNode {
87
+ id: string;
88
+ value: number;
89
+ item: ReportMetricRow;
90
+ children?: Array<TreeNode>;
91
+ }
92
+
93
+ interface RootTree {
94
+ id: string;
95
+ value: number;
96
+ children: Array<TreeNode>;
97
+ }
98
+
99
+ interface LeafProps {
100
+ x: number;
101
+ y: number;
102
+ width: number;
103
+ height: number;
104
+ data: TreeNode;
105
+ onClick?: (id: string) => void;
106
+ }
107
+
108
+ const Tile = (props: LeafProps) => {
109
+ const { x, y, width, height, data, onClick } = props;
110
+ const { item } = data;
111
+ const runInfo = item.runs?.[0] as MetricRunInfo;
112
+
113
+ const [isOpen, setIsOpen] = useState(false);
114
+ const arrowRef = useRef(null);
115
+ const { refs, floatingStyles, context, middlewareData } = useFloating({
116
+ placement: 'top',
117
+ open: isOpen,
118
+ onOpenChange: setIsOpen,
119
+ middleware: [arrow({ element: arrowRef }), offset(16)],
120
+ });
121
+ const clientPoint = useClientPoint(context);
122
+ const hover = useHover(context);
123
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover, clientPoint]);
124
+
125
+ const tileSizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
126
+
127
+ const handleOnClick = useCallback(() => {
128
+ if (onClick) {
129
+ onClick(item.key);
130
+ }
131
+ }, [item.key]);
132
+
133
+ return (
134
+ <g
135
+ className={cx(
136
+ css.tile,
137
+ !item.changed && css['tile-NO_CHANGE'],
138
+ css[`tile-${runInfo.deltaType}`],
139
+ tileSizeDisplay === 'small' && css.tileSmall,
140
+ )}
141
+ >
142
+ <rect x={x} y={y} width={width} height={height} className={css.tileOverlay} />
143
+ <foreignObject height={height} width={width} x={x} y={y}>
144
+ <TileButton
145
+ onClick={handleOnClick}
146
+ className={css.tileContent}
147
+ ref={refs.setReference}
148
+ {...getReferenceProps()}
149
+ >
150
+ {tileSizeDisplay !== 'minimal' && (
151
+ <div className={css.tileContentWrapper}>
152
+ <p className={css.tileContentLabel}>{item.label}</p>
153
+ <p className={css.tileContentValue}>
154
+ <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
155
+ <Delta
156
+ className={css.tileContentDelta}
157
+ displayValue={runInfo.displayDeltaPercentage}
158
+ deltaType={runInfo.deltaType}
159
+ />
160
+ </p>
161
+ </div>
162
+ )}
163
+ </TileButton>
164
+ {isOpen && (
165
+ <FloatingPortal>
166
+ <div
167
+ ref={refs.setFloating}
168
+ style={floatingStyles}
169
+ {...getFloatingProps()}
170
+ className={css.tooltip}
171
+ >
172
+ <div
173
+ ref={arrowRef}
174
+ style={{ left: middlewareData.arrow?.x }}
175
+ className={css.tooltipArrow}
176
+ />
177
+ <TooltipContent data={item} />
178
+ </div>
179
+ </FloatingPortal>
180
+ )}
181
+ </foreignObject>
182
+ </g>
183
+ );
184
+ };
185
+
186
+ interface MetricsTreemapProps {
187
+ items: Array<ReportMetricRow>;
188
+ emptyMessage?: React.ReactNode;
189
+ onItemClick?: (entryId: string) => void;
190
+ }
191
+
192
+ export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
193
+ const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
194
+
195
+ const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
196
+
197
+ const treemapHierarchy = useMemo(() => {
198
+ const children = items.map((item) => {
199
+ // set tile value to the largest run value to display the impact of deleted items
200
+ const values = item.runs.map((run) => run?.value || 0);
201
+ const value = Math.max(...values);
202
+
203
+ return {
204
+ id: item.key,
205
+ value,
206
+ item,
207
+ };
208
+ });
209
+
210
+ const treemapData = {
211
+ id: 'root',
212
+ name: 'root',
213
+ value: 0,
214
+ children,
215
+ };
216
+
217
+ const customHierarchy = hierarchy<RootTree>(treemapData);
218
+ customHierarchy.sum((node) => node.value);
219
+ // @ts-expect-error
220
+ customHierarchy.sort((a, b) => b.value - a.value);
221
+
222
+ return customHierarchy;
223
+ }, [items]);
224
+
225
+ const cells = useMemo(() => {
226
+ const createTremapLayout = treemap()
227
+ .size([width, height])
228
+ .tile(treemapSquarify.ratio(SQUARIFY_RATIO))
229
+ .paddingOuter(PADDING_OUTER)
230
+ .paddingInner(PADDING_INNER);
231
+ // @ts-expect-error
232
+ const tremapLayout = createTremapLayout(treemapHierarchy);
233
+
234
+ return tremapLayout.leaves().filter((leaf) => Boolean(leaf.parent));
235
+ }, [height, width, treemapHierarchy]);
236
+
237
+ return (
238
+ <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
239
+ {cells.length > 0 ? (
240
+ <svg
241
+ width={width}
242
+ height={height}
243
+ viewBox={`0 0 ${width} ${height}`}
244
+ className={css.canvas}
245
+ >
246
+ {cells.map((leaf) => {
247
+ if (!leaf.parent) {
248
+ return null;
249
+ }
250
+
251
+ return (
252
+ <Tile
253
+ x={leaf.x0}
254
+ y={leaf.y0}
255
+ width={leaf.x1 - leaf.x0}
256
+ height={leaf.y1 - leaf.y0}
257
+ data={leaf.data as TreeNode}
258
+ key={leaf.id}
259
+ onClick={onItemClick}
260
+ />
261
+ );
262
+ })}
263
+ </svg>
264
+ ) : (
265
+ <div className={css.emptyMessage}>
266
+ <div className={css.emptyMessageWrapper}>{emptyMessage}</div>
267
+ </div>
268
+ )}
269
+ </div>
270
+ );
271
+ };
@@ -40,3 +40,8 @@ export const SORT = {
40
40
  ASC: 'asc',
41
41
  DESC: 'desc',
42
42
  };
43
+
44
+ export enum MetricsDisplayType {
45
+ TABLE = 'table',
46
+ TREEMAP = 'treemap',
47
+ }