diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html index f169fabfaebb..8b10842f9bb0 100644 --- a/lighthouse-treemap/app/index.html +++ b/lighthouse-treemap/app/index.html @@ -22,38 +22,42 @@
- - + + + + + Lighthouse Treemap + - Lighthouse Treemap +
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js index 9c04b11a306c..b83ff35f4c9a 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() { @@ -77,6 +83,50 @@ 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) { + const optionEl = TreemapUtil.createChildOf(bundleSelectorEl, 'option'); + optionEl.value = String(selectors.length); + selectors.push(selector); + optionEl.textContent = text; + } + + 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; + + makeOption({type: 'depthOneNode', value: depthOneNode.name}, depthOneNode.name); + } + } + + const currentSelectorIndex = selectors.findIndex(s => { + return this.selector && + s.type === this.selector.type && + s.value === this.selector.value; + }); + bundleSelectorEl.value = String(currentSelectorIndex !== -1 ? currentSelectorIndex : 0); + bundleSelectorEl.addEventListener('change', () => { + const index = Number(bundleSelectorEl.value); + const selector = selectors[index]; + this.setSelector(selector); + this.render(); + }); } initListeners() { @@ -173,32 +223,88 @@ class TreemapViewer { return viewModes; } - 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.Selector} selector + */ + setSelector(selector) { + this.selector = selector; + + 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; + } + } + } - this.treemap = new webtreemap.TreeMap(this.currentTreemapRoot, { - padding: [16, 3, 3, 3], - spacing: 10, - caption: node => this.makeCaption(node), - }); + if (!node) { + throw new Error('unknown depthOneNode: ' + selector.value); + } - this.el.innerHTML = ''; - this.treemap.render(this.el); + this.currentTreemapRoot = node; + } else { + throw new Error('unknown selector: ' + JSON.stringify(selector)); + } - applyActiveClass(this.currentViewMode.id); - TreemapUtil.find('.webtreemap-node').classList.add('webtreemap-node--root'); + this.viewModes = this.createViewModes(); + if (!this.currentViewMode) this.currentViewMode = this.viewModes[0]; + } - this.updateColors(); + /** + * @param {LH.Treemap.ViewMode} viewMode + */ + setViewMode(viewMode) { + this.currentViewMode = viewMode; + } + + 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.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'); + } + + if (rootChanged || viewChanged) { + this.updateColors(); + applyActiveClass(this.currentViewMode.id); + } + + this.previousRenderState = { + root: this.currentTreemapRoot, + viewMode: this.currentViewMode, + }; } resize() { @@ -292,7 +398,7 @@ function renderViewModeButtons(viewModes) { }); inputEl.addEventListener('click', () => { - treemapViewer.currentViewMode = viewMode; + treemapViewer.setViewMode(viewMode); treemapViewer.render(); }); } diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css index c3d33b588847..60c1b826df97 100644 --- a/lighthouse-treemap/app/styles/treemap.css +++ b/lighthouse-treemap/app/styles/treemap.css @@ -41,10 +41,10 @@ body { .lh-header--section { display: flex; align-items: center; - font-size: 20px; + justify-content: space-between; } .lh-header--section:nth-of-type(2) { - justify-content: space-between; + font-size: 20px; } .lh-header--url { font-weight: bold; @@ -54,6 +54,11 @@ body { white-space: nowrap; } +.bundle-selector { + width: 50%; + padding: 2px; +} + .lh-topbar__logo { width: 24px; height: 24px; 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 663cc0906b02..71453ce623d6 100644 --- a/types/treemap.d.ts +++ b/types/treemap.d.ts @@ -12,6 +12,11 @@ declare global { type NodePath = string[]; + interface Selector { + type: 'depthOneNode' | 'group'; + value: string; + } + interface ViewMode { id: 'all' | 'unused-bytes'; label: string;