From 85231a1c681b05a0987a97a816de697d588a7fec Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Apr 2021 17:15:34 -0600 Subject: [PATCH 1/5] misc: treemap selector --- lighthouse-treemap/app/index.html | 1 + lighthouse-treemap/app/src/main.js | 94 +++++++++++++++++++++++++++++- lighthouse-treemap/app/src/util.js | 18 ++++++ types/treemap.d.ts | 6 ++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html index f169fabfaebb..fc2570f5b6ff 100644 --- a/lighthouse-treemap/app/index.html +++ b/lighthouse-treemap/app/index.html @@ -60,6 +60,7 @@ · +
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index 9c04b11a306c..1c852e70b280 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -77,6 +77,60 @@ class TreemapViewer { const bytes = this.wrapNodesInNewRootNode(this.depthOneNodesByGroup.scripts).resourceBytes; TreemapUtil.find('.lh-header--size').textContent = TreemapUtil.formatBytes(bytes); + + this.createBundleSelector(); + } + + createBundleSelector() { + const bundleSelectorEl = TreemapUtil.find('select.bundle-selector'); + bundleSelectorEl.innerHTML = ''; // Clear just in case document was saved with Ctrl+S. + + /** @type {LH.Treemap.Selector[]} */ + const selectors = []; + + /** + * @param {LH.Treemap.Selector} selector + * @param {string} text + */ + function makeOption(selector, text) { + if (!['depthOneNode', 'group'].includes(selector.type)) { + throw new Error('unexpected selector type ' + selector.type); + } + + const optionEl = TreemapUtil.createChildOf(bundleSelectorEl, 'option'); + optionEl.value = String(selectors.length); + selectors.push(selector); + optionEl.innerText = text; + } + + function onChange() { + const index = Number(bundleSelectorEl.value); + const selector = selectors[index]; + treemapViewer.setViewMode({ + ...treemapViewer.currentViewMode, + selector, + }); + } + + for (const [group, depthOneNodes] of Object.entries(this.depthOneNodesByGroup)) { + makeOption({type: 'group', value: group}, `All ${group}`); + for (const depthOneNode of depthOneNodes) { + // Only add bundles. + if (!depthOneNode.children) continue; + + // const title = (aggregateNodes ? '- ' : '') + TreemapUtil.elide(rootNode.name, 80); + const title = TreemapUtil.elide(depthOneNode.name, 80); + makeOption({type: 'depthOneNode', value: depthOneNode.name}, title); + } + } + + const currentSelectorIndex = selectors.findIndex(s => { + return this.currentViewMode.selector && + s.type === this.currentViewMode.selector.type && + s.value === this.currentViewMode.selector.value; + }); + bundleSelectorEl.value = String(currentSelectorIndex !== -1 ? currentSelectorIndex : 0); + bundleSelectorEl.addEventListener('change', onChange); } initListeners() { @@ -173,6 +227,40 @@ class TreemapViewer { return viewModes; } + /** + * @param {LH.Treemap.ViewMode} viewMode + */ + setViewMode(viewMode) { + this.currentViewMode = viewMode; + + const selector = this.currentViewMode.selector || {type: 'group', value: 'scripts'}; + + if (selector.type === 'group') { + this.currentTreemapRoot = + this.wrapNodesInNewRootNode(this.depthOneNodesByGroup[selector.value]); + } else if (selector.type === 'depthOneNode') { + let node; + outer: for (const depthOneNodes of Object.values(this.depthOneNodesByGroup)) { + for (const depthOneNode of depthOneNodes) { + if (depthOneNode.name === selector.value) { + node = depthOneNode; + break outer; + } + } + } + + if (!node) { + throw new Error('unknown depthOneNode: ' + selector.value); + } + + this.currentTreemapRoot = node; + } else { + throw new Error('unknown selector: ' + JSON.stringify(selector)); + } + + this.render(); + } + render() { TreemapUtil.walk(this.currentTreemapRoot, node => { // @ts-ignore: webtreemap will store `dom` on the data to speed up operations. @@ -292,8 +380,10 @@ function renderViewModeButtons(viewModes) { }); inputEl.addEventListener('click', () => { - treemapViewer.currentViewMode = viewMode; - treemapViewer.render(); + treemapViewer.setViewMode({ + ...viewMode, + selector: treemapViewer.currentViewMode.selector, + }); }); } diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js index d09cceb4f5d1..74a3db40ef23 100644 --- a/lighthouse-treemap/app/src/util.js +++ b/lighthouse-treemap/app/src/util.js @@ -31,6 +31,24 @@ class TreemapUtil { } } + /** + * @param {LH.Treemap.Node} node + * @param {string[]} path + */ + static findNode(node, path) { + let result = node; + for (const pathComponent of path) { + if (!result.children) return; + + const next = result.children.find(child => child.name === pathComponent); + if (!next) return; + + result = next; + } + + return result; + } + /** * @param {string[]} path1 * @param {string[]} path2 diff --git a/types/treemap.d.ts b/types/treemap.d.ts index 663cc0906b02..3ef4f980dca6 100644 --- a/types/treemap.d.ts +++ b/types/treemap.d.ts @@ -12,8 +12,14 @@ declare global { type NodePath = string[]; + interface Selector { + type: 'depthOneNode' | 'group'; + value: string; + } + interface ViewMode { id: 'all' | 'unused-bytes'; + selector?: Selector; label: string; subLabel: string; partitionBy?: 'resourceBytes' | 'unusedBytes'; From 531c3f007707a9a95037487f14fb7fa987c9df79 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Apr 2021 17:23:39 -0600 Subject: [PATCH 2/5] styles --- lighthouse-treemap/app/index.html | 67 ++++++++++++----------- lighthouse-treemap/app/src/main.js | 4 +- lighthouse-treemap/app/src/util.js | 18 ------ lighthouse-treemap/app/styles/treemap.css | 9 ++- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html index fc2570f5b6ff..8b10842f9bb0 100644 --- a/lighthouse-treemap/app/index.html +++ b/lighthouse-treemap/app/index.html @@ -22,45 +22,48 @@
- - + + + + + Lighthouse Treemap + - Lighthouse Treemap +
· -
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index 1c852e70b280..ed95120d2a9b 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -118,9 +118,7 @@ class TreemapViewer { // Only add bundles. if (!depthOneNode.children) continue; - // const title = (aggregateNodes ? '- ' : '') + TreemapUtil.elide(rootNode.name, 80); - const title = TreemapUtil.elide(depthOneNode.name, 80); - makeOption({type: 'depthOneNode', value: depthOneNode.name}, title); + makeOption({type: 'depthOneNode', value: depthOneNode.name}, depthOneNode.name); } } diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js index 74a3db40ef23..d09cceb4f5d1 100644 --- a/lighthouse-treemap/app/src/util.js +++ b/lighthouse-treemap/app/src/util.js @@ -31,24 +31,6 @@ class TreemapUtil { } } - /** - * @param {LH.Treemap.Node} node - * @param {string[]} path - */ - static findNode(node, path) { - let result = node; - for (const pathComponent of path) { - if (!result.children) return; - - const next = result.children.find(child => child.name === pathComponent); - if (!next) return; - - result = next; - } - - return result; - } - /** * @param {string[]} path1 * @param {string[]} path2 diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css index c3d33b588847..3e4e3d2a82e6 100644 --- a/lighthouse-treemap/app/styles/treemap.css +++ b/lighthouse-treemap/app/styles/treemap.css @@ -41,10 +41,8 @@ body { .lh-header--section { display: flex; align-items: center; - font-size: 20px; -} -.lh-header--section:nth-of-type(2) { justify-content: space-between; + font-size: 20px; } .lh-header--url { font-weight: bold; @@ -54,6 +52,11 @@ body { white-space: nowrap; } +.bundle-selector { + width: 50%; + padding: 2px; +} + .lh-topbar__logo { width: 24px; height: 24px; From 9857d31453262512497e1f8c99f2b49b4e5db353 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 13 Apr 2021 17:26:55 -0600 Subject: [PATCH 3/5] oops --- lighthouse-treemap/app/styles/treemap.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css index 3e4e3d2a82e6..60c1b826df97 100644 --- a/lighthouse-treemap/app/styles/treemap.css +++ b/lighthouse-treemap/app/styles/treemap.css @@ -42,6 +42,8 @@ body { display: flex; align-items: center; justify-content: space-between; +} +.lh-header--section:nth-of-type(2) { font-size: 20px; } .lh-header--url { From 8e60e086a94c6e9c5098143bab0d78be624317c3 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 16 Apr 2021 18:43:16 -0600 Subject: [PATCH 4/5] update --- lighthouse-treemap/app/src/main.js | 128 +++++++++++++++----------- lighthouse-treemap/types/treemap.d.ts | 5 + types/treemap.d.ts | 1 - 3 files changed, 79 insertions(+), 55 deletions(-) diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index ed95120d2a9b..79cac3689eff 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -55,19 +55,25 @@ class TreemapViewer { this.el = el; this.getHueForKey = TreemapUtil.stableHasher(TreemapUtil.COLOR_HUES); - // TODO: make "DataSelector" to switch between different groups or specific d1 nodes - const group = 'scripts'; - const depthOneNodes = this.depthOneNodesByGroup[group]; - this.currentTreemapRoot = this.wrapNodesInNewRootNode(depthOneNodes); - TreemapUtil.walk(this.currentTreemapRoot, (node, path) => this.nodeToPathMap.set(node, path)); - - this.viewModes = this.createViewModes(); - this.currentViewMode = this.viewModes[0]; + /* eslint-disable no-unused-expressions */ + /** @type {LH.Treemap.Node} */ + this.currentTreemapRoot; + /** @type {LH.Treemap.ViewMode} */ + this.currentViewMode; + /** @type {LH.Treemap.Selector} */ + this.selector; + /** @type {LH.Treemap.ViewMode[]} */ + this.viewModes; + /** @type {RenderState=} */ + this.previousRenderState; + /** @type {WebTreeMap} */ + this.treemap; + /* eslint-enable no-unused-expressions */ - renderViewModeButtons(this.viewModes); this.createHeader(); - this.render(); this.initListeners(); + this.setSelector({type: 'group', value: 'scripts'}); + this.render(); } createHeader() { @@ -93,23 +99,17 @@ class TreemapViewer { * @param {string} text */ function makeOption(selector, text) { - if (!['depthOneNode', 'group'].includes(selector.type)) { - throw new Error('unexpected selector type ' + selector.type); - } - const optionEl = TreemapUtil.createChildOf(bundleSelectorEl, 'option'); optionEl.value = String(selectors.length); selectors.push(selector); - optionEl.innerText = text; + optionEl.textContent = text; } function onChange() { const index = Number(bundleSelectorEl.value); const selector = selectors[index]; - treemapViewer.setViewMode({ - ...treemapViewer.currentViewMode, - selector, - }); + treemapViewer.setSelector(selector); + treemapViewer.render(); } for (const [group, depthOneNodes] of Object.entries(this.depthOneNodesByGroup)) { @@ -123,9 +123,9 @@ class TreemapViewer { } const currentSelectorIndex = selectors.findIndex(s => { - return this.currentViewMode.selector && - s.type === this.currentViewMode.selector.type && - s.value === this.currentViewMode.selector.value; + return this.selector && + s.type === this.selector.type && + s.value === this.selector.value; }); bundleSelectorEl.value = String(currentSelectorIndex !== -1 ? currentSelectorIndex : 0); bundleSelectorEl.addEventListener('change', onChange); @@ -226,12 +226,10 @@ class TreemapViewer { } /** - * @param {LH.Treemap.ViewMode} viewMode + * @param {LH.Treemap.Selector} selector */ - setViewMode(viewMode) { - this.currentViewMode = viewMode; - - const selector = this.currentViewMode.selector || {type: 'group', value: 'scripts'}; + setSelector(selector) { + this.selector = selector; if (selector.type === 'group') { this.currentTreemapRoot = @@ -256,35 +254,59 @@ class TreemapViewer { throw new Error('unknown selector: ' + JSON.stringify(selector)); } - this.render(); + this.viewModes = this.createViewModes(); + if (!this.currentViewMode) this.currentViewMode = this.viewModes[0]; } - render() { - TreemapUtil.walk(this.currentTreemapRoot, node => { - // @ts-ignore: webtreemap will store `dom` on the data to speed up operations. - // However, when we change the underlying data representation, we need to delete - // all the cached DOM elements. Otherwise, the rendering will be incorrect when, - // for example, switching between "All JavaScript" and a specific bundle. - delete node.dom; - - // @ts-ignore: webtreemap uses `size` to partition the treemap. - node.size = node[this.currentViewMode.partitionBy || 'resourceBytes'] || 0; - }); - webtreemap.sort(this.currentTreemapRoot); + /** + * @param {LH.Treemap.ViewMode} viewMode + */ + setViewMode(viewMode) { + this.currentViewMode = viewMode; + } - this.treemap = new webtreemap.TreeMap(this.currentTreemapRoot, { - padding: [16, 3, 3, 3], - spacing: 10, - caption: node => this.makeCaption(node), - }); + render() { + const rootChanged = + !this.previousRenderState || this.previousRenderState.root !== this.currentTreemapRoot; + const viewChanged = + !this.previousRenderState || this.previousRenderState.viewMode !== this.currentViewMode; + + if (rootChanged) { + this.nodeToPathMap = new Map(); + TreemapUtil.walk(this.currentTreemapRoot, (node, path) => this.nodeToPathMap.set(node, path)); + renderViewModeButtons(this.viewModes); + + TreemapUtil.walk(this.currentTreemapRoot, node => { + // @ts-ignore: webtreemap will store `dom` on the data to speed up operations. + // However, when we change the underlying data representation, we need to delete + // all the cached DOM elements. Otherwise, the rendering will be incorrect when, + // for example, switching between "All JavaScript" and a specific bundle. + delete node.dom; + + // @ts-ignore: webtreemap uses `size` to partition the treemap. + node.size = node[this.currentViewMode.partitionBy || 'resourceBytes'] || 0; + }); + webtreemap.sort(this.currentTreemapRoot); - this.el.innerHTML = ''; - this.treemap.render(this.el); + this.treemap = new webtreemap.TreeMap(this.currentTreemapRoot, { + padding: [16, 3, 3, 3], + spacing: 10, + caption: node => this.makeCaption(node), + }); + this.el.innerHTML = ''; + this.treemap.render(this.el); + TreemapUtil.find('.webtreemap-node').classList.add('webtreemap-node--root'); + } - applyActiveClass(this.currentViewMode.id); - TreemapUtil.find('.webtreemap-node').classList.add('webtreemap-node--root'); + if (rootChanged || viewChanged) { + this.updateColors(); + applyActiveClass(this.currentViewMode.id); + } - this.updateColors(); + this.previousRenderState = { + root: this.currentTreemapRoot, + viewMode: this.currentViewMode, + }; } resize() { @@ -378,10 +400,8 @@ function renderViewModeButtons(viewModes) { }); inputEl.addEventListener('click', () => { - treemapViewer.setViewMode({ - ...viewMode, - selector: treemapViewer.currentViewMode.selector, - }); + treemapViewer.setViewMode(viewMode); + treemapViewer.render(); }); } diff --git a/lighthouse-treemap/types/treemap.d.ts b/lighthouse-treemap/types/treemap.d.ts index 77bf4fed0fd5..32fea0109098 100644 --- a/lighthouse-treemap/types/treemap.d.ts +++ b/lighthouse-treemap/types/treemap.d.ts @@ -14,6 +14,11 @@ declare global { showNode?(node: LH.Treemap.Node): boolean; } + interface RenderState { + root: LH.Treemap.Node; + viewMode: LH.Treemap.ViewMode; + } + var webtreemap: { TreeMap: typeof WebTreeMap; render(el: HTMLElement, data: any, options: WebTreeMapOptions): void; diff --git a/types/treemap.d.ts b/types/treemap.d.ts index 3ef4f980dca6..71453ce623d6 100644 --- a/types/treemap.d.ts +++ b/types/treemap.d.ts @@ -19,7 +19,6 @@ declare global { interface ViewMode { id: 'all' | 'unused-bytes'; - selector?: Selector; label: string; subLabel: string; partitionBy?: 'resourceBytes' | 'unusedBytes'; From f809afdf49a3df70279eab464910cdb528ab86d0 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Thu, 22 Apr 2021 14:04:33 -0600 Subject: [PATCH 5/5] update --- lighthouse-treemap/app/src/main.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index 79cac3689eff..b83ff35f4c9a 100644 --- a/lighthouse-treemap/app/src/main.js +++ b/lighthouse-treemap/app/src/main.js @@ -105,13 +105,6 @@ class TreemapViewer { optionEl.textContent = text; } - function onChange() { - const index = Number(bundleSelectorEl.value); - const selector = selectors[index]; - treemapViewer.setSelector(selector); - treemapViewer.render(); - } - for (const [group, depthOneNodes] of Object.entries(this.depthOneNodesByGroup)) { makeOption({type: 'group', value: group}, `All ${group}`); for (const depthOneNode of depthOneNodes) { @@ -128,7 +121,12 @@ class TreemapViewer { s.value === this.selector.value; }); bundleSelectorEl.value = String(currentSelectorIndex !== -1 ? currentSelectorIndex : 0); - bundleSelectorEl.addEventListener('change', onChange); + bundleSelectorEl.addEventListener('change', () => { + const index = Number(bundleSelectorEl.value); + const selector = selectors[index]; + this.setSelector(selector); + this.render(); + }); } initListeners() {