From 65e674b4ec99fa2810ace390e9870619d91dab7d Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 29 Apr 2021 10:12:19 -0600 Subject: [PATCH] misc(treemap): add data table (#12363) --- build/build-treemap.js | 7 ++ lighthouse-treemap/app/index.html | 11 +- lighthouse-treemap/app/src/main.js | 140 +++++++++++++++++++++- lighthouse-treemap/app/styles/treemap.css | 65 +++++++++- package.json | 2 + yarn.lock | 10 ++ 6 files changed, 222 insertions(+), 13 deletions(-) diff --git a/build/build-treemap.js b/build/build-treemap.js index 9f429063a76f..a2a02987b174 100644 --- a/build/build-treemap.js +++ b/build/build-treemap.js @@ -17,10 +17,17 @@ async function run() { appDir: `${__dirname}/../lighthouse-treemap/app`, html: {path: 'index.html'}, stylesheets: [ + fs.readFileSync(require.resolve('tabulator-tables/dist/css/tabulator.min.css'), 'utf8'), {path: 'styles/*'}, ], javascripts: [ + /* eslint-disable max-len */ fs.readFileSync(require.resolve('webtreemap-cdt'), 'utf8'), + fs.readFileSync(require.resolve('tabulator-tables/dist/js/tabulator_core.js'), 'utf8'), + fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/sort.js'), 'utf8'), + fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/format.js'), 'utf8'), + fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/resize_columns.js'), 'utf8'), + /* eslint-enable max-len */ {path: 'src/*'}, ], assets: [ diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html index 8b10842f9bb0..c84042031d11 100644 --- a/lighthouse-treemap/app/index.html +++ b/lighthouse-treemap/app/index.html @@ -18,7 +18,7 @@ -
+
@@ -57,7 +57,10 @@ Lighthouse Treemap - + + + +
@@ -76,6 +79,10 @@
+ +
+ +
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index b83ff35f4c9a..d3a47e8d986e 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -7,7 +7,7 @@ /* eslint-env browser */ -/* globals webtreemap TreemapUtil */ +/* globals webtreemap TreemapUtil Tabulator Cell Row */ const UNUSED_BYTES_IGNORE_THRESHOLD = 20 * 1024; const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO = 0.5; @@ -15,6 +15,16 @@ const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO = 0.5; /** @type {TreemapViewer} */ let treemapViewer; +// Make scrolling in Tabulator more performant. +// @ts-expect-error +Cell.prototype.clearHeight = () => {}; +// @ts-expect-error +Row.prototype.calcHeight = function() { + this.height = 24; + this.outerHeight = 24; + this.heightStyled = '24px'; +}; + class TreemapViewer { /** * @param {LH.Treemap.Options} options @@ -85,6 +95,9 @@ class TreemapViewer { TreemapUtil.find('.lh-header--size').textContent = TreemapUtil.formatBytes(bytes); this.createBundleSelector(); + + const toggleTableBtn = TreemapUtil.find('.lh-button--toggle-table'); + toggleTableBtn.addEventListener('click', () => treemapViewer.toggleTable()); } createBundleSelector() { @@ -130,11 +143,11 @@ class TreemapViewer { } initListeners() { - window.addEventListener('resize', () => { - this.resize(); - }); - const treemapEl = TreemapUtil.find('.lh-treemap'); + + const resizeObserver = new ResizeObserver(() => this.resize()); + resizeObserver.observe(treemapEl); + treemapEl.addEventListener('click', (e) => { if (!(e.target instanceof HTMLElement)) return; const nodeEl = e.target.closest('.webtreemap-node'); @@ -294,6 +307,8 @@ class TreemapViewer { this.el.innerHTML = ''; this.treemap.render(this.el); TreemapUtil.find('.webtreemap-node').classList.add('webtreemap-node--root'); + + this.createTable(); } if (rootChanged || viewChanged) { @@ -307,11 +322,126 @@ class TreemapViewer { }; } + createTable() { + const tableEl = TreemapUtil.find('.lh-table'); + tableEl.innerHTML = ''; + + /** @type {Array<{name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */ + const data = []; + TreemapUtil.walk(this.currentTreemapRoot, (node, path) => { + if (node.children) return; + + const depthOneNode = this.nodeToDepthOneNodeMap.get(node); + const bundleNode = depthOneNode && depthOneNode.children ? depthOneNode : undefined; + + let name; + if (bundleNode) { + const bundleNodePath = this.nodeToPathMap.get(bundleNode); + const amountToTrim = bundleNodePath ? bundleNodePath.length : 0; // should never be 0. + name = `(bundle) ${path.slice(amountToTrim).join('/')}`; + } else { + // Elide the first path component, which is common to all nodes. + if (path[0] === this.currentTreemapRoot.name) { + name = path.slice(1).join('/'); + } else { + name = path.join('/'); + } + + // Elide the document URL. + if (name.startsWith(this.currentTreemapRoot.name)) { + name = name.replace(this.currentTreemapRoot.name, '//'); + } + } + + data.push({ + name, + bundleNode, + resourceBytes: node.resourceBytes, + unusedBytes: node.unusedBytes, + }); + }); + + /** @param {Tabulator.CellComponent} cell */ + const makeNameTooltip = (cell) => { + /** @type {typeof data[number]} */ + const dataRow = cell.getRow().getData(); + if (!dataRow.bundleNode) return ''; + + return `${dataRow.bundleNode.name} (bundle) ${dataRow.name}`; + }; + + /** @param {Tabulator.CellComponent} cell */ + const makeCoverageTooltip = (cell) => { + /** @type {typeof data[number]} */ + const dataRow = cell.getRow().getData(); + if (!dataRow.unusedBytes) return ''; + + const percent = Math.floor(100 * dataRow.unusedBytes / dataRow.resourceBytes); + return `${percent}% bytes unused`; + }; + + const gridEl = document.createElement('div'); + tableEl.append(gridEl); + + const children = this.currentTreemapRoot.children || []; + const maxSize = Math.max(...children.map(node => node.resourceBytes)); + + this.table = new Tabulator(gridEl, { + data, + height: '100%', + layout: 'fitColumns', + tooltips: true, + addRowPos: 'top', + resizableColumns: true, + initialSort: [ + {column: 'resourceBytes', dir: 'desc'}, + ], + columns: [ + {title: 'Name', field: 'name', widthGrow: 5, tooltip: makeNameTooltip}, + {title: 'Size', field: 'resourceBytes', headerSortStartingDir: 'desc', formatter: cell => { + const value = cell.getValue(); + return TreemapUtil.formatBytes(value); + }}, + // eslint-disable-next-line max-len + {title: 'Unused', field: 'unusedBytes', widthGrow: 1, sorterParams: {alignEmptyValues: 'bottom'}, headerSortStartingDir: 'desc', formatter: cell => { + const value = cell.getValue(); + if (value === undefined) return ''; + return TreemapUtil.formatBytes(value); + }}, + // eslint-disable-next-line max-len + {title: 'Coverage', widthGrow: 3, headerSort: false, tooltip: makeCoverageTooltip, formatter: cell => { + /** @type {typeof data[number]} */ + const dataRow = cell.getRow().getData(); + + const el = TreemapUtil.createElement('div', 'lh-coverage-bar'); + if (dataRow.unusedBytes === undefined) return el; + + el.style.setProperty('--max', String(maxSize)); + el.style.setProperty('--used', String(dataRow.resourceBytes - dataRow.unusedBytes)); + el.style.setProperty('--unused', String(dataRow.unusedBytes)); + + TreemapUtil.createChildOf(el, 'div', 'lh-coverage-bar--used'); + TreemapUtil.createChildOf(el, 'div', 'lh-coverage-bar--unused'); + + return el; + }}, + ], + }); + } + + toggleTable() { + const mainEl = TreemapUtil.find('main'); + mainEl.classList.toggle('lh-main--show-table'); + const buttonEl = TreemapUtil.find('.lh-button--toggle-table'); + buttonEl.classList.toggle('lh-button--active'); + } + resize() { if (!this.treemap) throw new Error('must call .render() first'); this.treemap.layout(this.currentTreemapRoot, this.el); this.updateColors(); + if (this.table) this.table.redraw(); } /** diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css index 60c1b826df97..0832785dc23a 100644 --- a/lighthouse-treemap/app/styles/treemap.css +++ b/lighthouse-treemap/app/styles/treemap.css @@ -10,10 +10,11 @@ --color-gray-600: #757575; --color-gray-900: #212121; + --control-background-color: #e7f1fe; --text-color-secondary: var(--color-gray-600); --text-color: var(--color-gray-900); - --view-mode-label-color-active: #2a67ce; - --view-mode-sublabel-color-active: #4484f3c7; + --text-color-active: #2a67ce; + --text-color-active-secondary: #4484f3c7; } body { @@ -26,6 +27,15 @@ body { flex-direction: column; } +.lh-button { + background: none; + color: var(--text-color-active); + border: solid 1px #e1e2e5; +} +.lh-button--active { + background-color: var(--control-background-color); +} + .lh-text-dim { color: var(--text-color-secondary); } @@ -36,8 +46,14 @@ body { grid-template-rows: 0.1fr 1fr 0fr; grid-column-gap: 0px; grid-row-gap: 0px; + transition: grid-template-rows 0.2s; + animation: 0.7s curtain cubic-bezier(0.86, 0, 0.07, 1) 0.4s both; +} +.lh-main--show-table { + grid-template-rows: 0.1fr 1fr 0.3fr; } +/* TODO: BEM is backwards here and many other places */ .lh-header--section { display: flex; align-items: center; @@ -53,6 +69,15 @@ body { overflow: hidden; white-space: nowrap; } +.lh-header__inputs { + display: flex; + justify-content: flex-end; +} + +.bundle-selector { + width: 50%; + padding: 2px; +} .bundle-selector { width: 50%; @@ -71,7 +96,35 @@ body { } .lh-treemap { - margin: 10px; + margin: 2px; + contain: content; +} + +.lh-table { + overflow-y: hidden; +} + +.tabulator { + /* Better default for unloaded portions of table. */ + background-color: #f3f3f3; + contain: strict; +} + +.lh-coverage-bar { + display: flex; + align-items: center; + height: 100%; +} +.lh-coverage-bar--used { + background-color: #63acbe; + width: calc(100% * var(--used) / var(--max)); + height: 7px; +} +.lh-coverage-bar--unused { + background-color: #ee442f; + width: calc(100% * var(--unused) / var(--max)); + height: 7px; + margin-left: 2px; } .view-mode { @@ -90,14 +143,14 @@ body { border-bottom-right-radius: 2px; } .view-mode--active { - background-color: #e7f1fe; + background-color: var(--control-background-color); border-color: #d2e3fc; } .view-mode--active .view-mode__label { - color: var(--view-mode-label-color-active); + color: var(--text-color-active); } .view-mode--active .view-mode__sublabel { - color: var(--view-mode-sublabel-color-active); + color: var(--text-color-active-secondary); } .view-mode__button { diff --git a/package.json b/package.json index 60e2991f0fc4..311a65aef9d0 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "@types/raven": "^2.5.1", "@types/resize-observer-browser": "^0.1.1", "@types/semver": "^5.5.0", + "@types/tabulator-tables": "^4.9.1", "@types/update-notifier": "^4.1.0", "@types/ws": "^4.0.1", "@types/yargs": "^15.0.11", @@ -147,6 +148,7 @@ "prettier": "^1.14.3", "pretty-json-stringify": "^0.0.2", "puppeteer": "^1.19.0", + "tabulator-tables": "^4.9.3", "terser": "^5.3.8", "typed-query-selector": "^2.4.0", "typescript": "4.2.3", diff --git a/yarn.lock b/yarn.lock index acbc4a152290..2c8616f94a24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/tabulator-tables@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@types/tabulator-tables/-/tabulator-tables-4.9.1.tgz#4fbc5465960598308260f50f840d610bcc8ffa3a" + integrity sha512-BfHDeVDfLF5H0HYFQ3ZEfv7J43sD8dw8fhci8juRB5uc0xQWboDd6f1hgLOwQioYhO9vWZEAWfuD6c1wl9qzmw== + "@types/through@*": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.29.tgz#72943aac922e179339c651fa34a4428a4d722f93" @@ -7544,6 +7549,11 @@ table@^6.0.4: slice-ansi "^4.0.0" string-width "^4.2.0" +tabulator-tables@^4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/tabulator-tables/-/tabulator-tables-4.9.3.tgz#89ea8f9bffc11ba9a789369b5165ac82da26f4f0" + integrity sha512-iwwQqAEGGxlgrBpcmJJvMJrfjGLcCXOB3AOb/DGkXqBy1YKoYA36hIl7qXGp6Jo8dSkzFAlDT6pKLZgyhs9OnQ== + tar-stream@^1.5.0: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"