Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc(treemap): add data table #12363

Merged
merged 18 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/build-treemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ 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: [
fs.readFileSync(require.resolve('webtreemap-cdt'), 'utf8'),
fs.readFileSync(require.resolve('tabulator-tables'), 'utf8'),
{path: 'src/*'},
],
assets: [
Expand Down
75 changes: 43 additions & 32 deletions lighthouse-treemap/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,49 @@
</head>

<body class="vars">
<main class="lh-main">
<main class="lh-main lh-main--show-table">
<div class="lh-settings">
<header>
<div class="lh-header--section">
<!-- Lighthouse logo. Stolen from templates.html -->
<svg class="lh-topbar__logo" viewBox="0 0 24 24">
<defs>
<linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
<stop stop-color="#FFF" stop-opacity=".1" offset="0%"/>
<stop stop-color="#FFF" stop-opacity="0" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"/>
<path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"/>
<path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"/>
<path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"/>
<path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"/>
<path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"/>
<path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"/>
</g>
</svg>
<span>
<!-- Lighthouse logo. Stolen from templates.html -->
<svg class="lh-topbar__logo" viewBox="0 0 24 24">
<defs>
<linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
<stop stop-color="#262626" stop-opacity="0" offset="100%"/>
</linearGradient>
<linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
<stop stop-color="#FFF" stop-opacity=".1" offset="0%"/>
<stop stop-color="#FFF" stop-opacity="0" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"/>
<path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"/>
<path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"/>
<path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"/>
<path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"/>
<path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"/>
<path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"/>
</g>
</svg>

<span>
<span class="lh-header--title lh-text-dim">Lighthouse Treemap</span>
<button class="lh-button lh-button--toggle-table">Toggle Table</button>
</span>
</span>

<span class="lh-header--title lh-text-dim">Lighthouse Treemap</span>
<select class="bundle-selector"></select>
</div>

<div class="lh-header--section">
Expand All @@ -72,6 +79,10 @@
<div class="lh-treemap">
<!-- treemap goes here -->
</div>

<div class="lh-table">
<!-- table goes here -->
</div>
</main>

<script src="src/bundled.js"></script>
Expand Down
197 changes: 188 additions & 9 deletions lighthouse-treemap/app/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/* eslint-env browser */

/* globals webtreemap TreemapUtil */
/* globals webtreemap TreemapUtil Tabulator */

const UNUSED_BYTES_IGNORE_THRESHOLD = 20 * 1024;
const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO = 0.5;
Expand Down Expand Up @@ -62,11 +62,13 @@ class TreemapViewer {
TreemapUtil.walk(this.currentTreemapRoot, (node, path) => this.nodeToPathMap.set(node, path));

this.viewModes = this.createViewModes();
this.currentViewMode = this.viewModes[0];

renderViewModeButtons(this.viewModes);

/** @type {LH.Treemap.ViewMode} */
this.currentViewMode; // eslint-disable-line no-unused-expressions
this.setViewMode(this.viewModes[0]);

this.createHeader();
this.render();
this.initListeners();
}

Expand All @@ -77,14 +79,73 @@ class TreemapViewer {

const bytes = this.wrapNodesInNewRootNode(this.depthOneNodesByGroup.scripts).resourceBytes;
TreemapUtil.find('.lh-header--size').textContent = TreemapUtil.formatBytes(bytes);

this.createBundleSelector();

const toggleTableBtn = TreemapUtil.find('.lh-button--toggle-table');
toggleTableBtn.addEventListener('click', () => treemapViewer.toggleTable());
}

initListeners() {
window.addEventListener('resize', () => {
this.resize();
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;

makeOption({type: 'depthOneNode', value: depthOneNode.name}, depthOneNode.name);
}
}

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() {
// 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');
Expand Down Expand Up @@ -173,6 +234,41 @@ 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();
this.createTable();
}

render() {
TreemapUtil.walk(this.currentTreemapRoot, node => {
// @ts-ignore: webtreemap will store `dom` on the data to speed up operations.
Expand Down Expand Up @@ -201,6 +297,87 @@ class TreemapViewer {
this.updateColors();
}

createTable() {
const tableEl = TreemapUtil.find('.lh-table');
tableEl.innerHTML = '';

/** @type {Array<{name: string, bytes: {resource: number, unused?: number}}>} */
const data = [];
let maxSize = 0;
TreemapUtil.walk(this.currentTreemapRoot, (node, path) => {
if (node.children) return;

if (node.resourceBytes) maxSize = Math.max(maxSize, node.resourceBytes);
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

// Elide the first path component, which is common to all nodes.
let name;
if (path[0] === this.currentTreemapRoot.name) {
name = path.slice(1).join('/');
} else {
name = path.join('/');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name creation approach ends up making long names where the important bit is probably at the end.

i don't have a great solution here, but i suspect a lot of the time it's gonna look like this:
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some stuff already to somewhat alleviate: the // is one, the other is trimming the selected bundle url.

perhaps if the name is a module in a bundle, the entire network URL part should be elided? a tooltip could then provide the url. the followup hover-on-row feature would also serve as a way to link a row to a particular bundle above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

(bundle) or (sm) or something else?

this change shortens things nicely. might still consider eliding long URLs of 3ps..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on vc, connor also suggested reusing the color we have above.. so we could do a dot/square using that color. i like that a lot.

the current (bundle) prefix is fine, i don't love it, but whatever.

i really think the tooltip should include the module URL. it makes the tooltip HUGE but i hate when text gets elided because of narrow columns (like in DevTools network panel) and i don't have a quick way of hovering to see what the full thing is.

}

// Elide the document URL.
if (name.startsWith(this.currentTreemapRoot.name)) {
name = name.replace(this.currentTreemapRoot.name, '//');
}

data.push({
name,
bytes: {resource: node.resourceBytes, unused: node.unusedBytes},
});
});

const gridEl = document.createElement('div');
tableEl.append(gridEl);

/**
* @param {typeof data[0]['bytes']} a
* @param {typeof data[0]['bytes']} b
* @return {number}
*/
const bytesSorter = (a, b) => a.resource - b.resource;

this.table = new Tabulator(gridEl, {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
data,
height: '100%',
layout: 'fitColumns',
tooltips: true,
addRowPos: 'top',
history: true,
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
resizableRows: true,
initialSort: [
{column: 'bytes', dir: 'desc'},
],
columns: [
{title: 'Name', field: 'name'},
{title: 'Size / Unused', field: 'bytes', sorter: bytesSorter, formatter: cell => {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const value = cell.getValue();
// eslint-disable-next-line max-len
return `${TreemapUtil.formatBytes(value.resource)} / ${TreemapUtil.formatBytes(value.unused)}`;
}},
{title: 'Coverage', field: 'bytes', sorter: bytesSorter, formatter: cell => {
const value = cell.getValue();

const el = TreemapUtil.createElement('div', 'lh-coverage-bar');
el.style.setProperty('--max', String(maxSize));
el.style.setProperty('--used', String(value.resource - value.unused));
el.style.setProperty('--unused', String(value.unused));

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');
}

resize() {
if (!this.treemap) throw new Error('must call .render() first');

Expand Down Expand Up @@ -292,8 +469,10 @@ function renderViewModeButtons(viewModes) {
});

inputEl.addEventListener('click', () => {
treemapViewer.currentViewMode = viewMode;
treemapViewer.render();
treemapViewer.setViewMode({
...viewMode,
selector: treemapViewer.currentViewMode.selector,
});
});
}

Expand Down
Loading