Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 23 additions & 4 deletions treemap/app/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ class TreemapViewer {
}
this.zoomedNodeEl = undefined;
dom.find('.lh-header').classList.remove('lh-header--zoomed');
this.updateColors();
}

applyActiveViewModeClass() {
Expand Down Expand Up @@ -892,18 +893,35 @@ class TreemapViewer {
}

/**
* Colors use OKLCH so that every hue has the same perceived lightness. Lightness
* decreases (and chroma increases) with depth, but both are clamped so that
* captions always have a good contrast ratio (>= 5.9:1 for #474747 text).
* @param {number} hue
* @param {number|null} depth
* @param {number|null} depth Depth relative to the node currently in view. If null,
* returns a more saturated color for highlights.
*/
getColorFromHue(hue, depth = null) {
if (depth === null) {
return TreemapUtil.hsl(hue, 60, 90);
return `oklch(0.88 0.12 ${hue})`;
}

return TreemapUtil.hsl(hue, 20 + depth * 5, 90 - depth * 5);
const lightness = Math.max(0.86, 0.97 - depth * 0.018);
const chroma = Math.min(0.06, 0.02 + depth * 0.008);
return `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue})`;
}

/**
* The depth of the node currently in view (the zoomed-in node, or the root),
* in terms of path length.
*/
getViewDepth() {
const zoomedNode = this.zoomedNodeEl && this.elementToNodeMap.get(this.zoomedNodeEl);
const zoomedPath = zoomedNode && this.nodeToPathMap.get(zoomedNode);
return zoomedPath ? zoomedPath.length : 1;
}

updateColors() {
const viewDepth = this.getViewDepth();
const hoveredDuplicateModule = this.hoveredDuplicateModuleName !== undefined ?
this.duplicateModules.get(this.hoveredDuplicateModuleName) :
undefined;
Expand Down Expand Up @@ -972,8 +990,9 @@ class TreemapViewer {
return;
}

// Depth is relative to what is in view, so zooming in brings back the lighter colors.
const depthOneNodeColor = hue !== undefined ?
this.getColorFromHue(hue, path.length) :
this.getColorFromHue(hue, Math.max(0, path.length - viewDepth)) :
'white';
node.dom.style.backgroundColor = depthOneNodeColor;
});
Expand Down
3 changes: 2 additions & 1 deletion treemap/app/styles/treemap.css
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,9 @@ body {
outline: 2px solid black;
}

/* A faint border keeps same-colored siblings visually separate. */
.lh-treemap--view-mode--all:not(.lh-treemap--hover-duplicate) .webtreemap-node {
border: none;
border-color: rgba(0, 0, 0, 0.08);
}

/* Duplicate modules are always outlined, in every view mode. */
Expand Down
34 changes: 34 additions & 0 deletions treemap/test/treemap-test-pptr.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,40 @@ describe('Lighthouse Treemap', () => {
expect(await getStyleWidth()).toBe(initialWidth);
});

it('keeps node colors light enough to read at every depth', async () => {
await page.goto(`${treemapUrl}?debug`, {
waitUntil: 'networkidle0',
timeout: 30000,
});

const getLightnesses = () => page.evaluate(() => {
return Array.from(document.querySelectorAll('.webtreemap-node'))
.map(el => el.style.backgroundColor)
.filter(color => color.startsWith('oklch('))
.map(color => Number(color.slice('oklch('.length).split(' ')[0]));
});

const topLevel = await getLightnesses();
expect(topLevel.length).toBeGreaterThan(0);
expect(Math.min(...topLevel)).toBeGreaterThanOrEqual(0.86);

// Zoom into a bundle with deeply nested modules.
const bundleCaption = await page.evaluateHandle(() => {
const captionEls = Array.from(document.querySelectorAll('.webtreemap-caption'));
return captionEls.find(el => el.textContent.includes('coursehero.com/ssi/508.'));
});
await bundleCaption.click();

const zoomed = await getLightnesses();
expect(Math.min(...zoomed)).toBeGreaterThanOrEqual(0.86);
// Depth is relative to the zoomed node, so the bundle itself is the lightest.
const bundleLightness = await bundleCaption.evaluate(el => {
const color = el.parentElement.style.backgroundColor;
return Number(color.slice('oklch('.length).split(' ')[0]);
});
expect(bundleLightness).toBe(Math.max(...zoomed));
});

describe('duplicate modules', () => {
const getHoverState = () => page.evaluate(() => {
const treemapEl = document.querySelector('.lh-treemap');
Expand Down
Loading