Skip to content
This repository was archived by the owner on Sep 7, 2026. It is now read-only.
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ a small hand-written stats file that covers every hint of the hints page and is
loadable in the app as the "hint test cases" example. CI runs this before the
build, so a failing test stops the run and nothing is deployed.

## Reading the graphs

The module and chunk graphs carry their meaning in colour, size and direction.
Each one draws a legend underneath itself, and it says the same as this:

| What you see | What it means |
| --- | --- |
| Dot colour and radius | The size of the module or chunk, green for the smallest and red for the largest in the build |
| Arrow | Points from the module that requires to the module required, and from a parent chunk to the chunk it loads |
| Edge colour (module graph) | When the module the arrow points at finished building, cyan early through magenta late. Only with `--profile`; without it the edge takes the colour of the module it points at |
| Edge width | Thinner when many modules require the same module, and thinner again when that module is loaded from an async chunk. In the chunk graph, thicker when a chunk has many parents |
| Black, red, green (module graph) | With a module open: the module itself, what requires it, and what it requires. With a chunk open: the modules in the chunk, and the edges into and out of it |
| Grey | Everything outside the current selection |

## Typical workflow

1. Build your application with webpack in profiling mode.
Expand Down
31 changes: 31 additions & 0 deletions app/graphs/chunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ var FA2Layout = require("graphology-layout-forceatlas2/worker");
var forceAtlas2 = require("graphology-layout-forceatlas2");
var rescale = require("./rescale");
var theme = require("./theme");
var legend = require("./legend");
var formatSize = require("../formatSize");
var percentageToColor = require("../percentageToColor").greenRed;

var element = document.getElementById("sigma-chunks");
var legendElement = legend.create(element);

var nodes = [];
var edges = [];
Expand Down Expand Up @@ -124,6 +127,32 @@ var s = new Sigma(graph, element, {
}
});

// Same wording rule as the module graph: every line below describes what the
// code above actually draws.
legend.render(legendElement, [
{
title: "chunk",
items: [
{
shape: "dot",
gradient: legend.gradient(percentageToColor),
text: "colour and radius: size, 0 to " + formatSize(maxSize)
}
]
},
{
title: "edge",
items: [
{ glyph: "\u2192", text: "points from a parent chunk to its child" },
{
shape: "line",
color: "rgba(120,130,140,0.6)",
text: "coloured like the child, thicker when it has many parents"
}
]
}
]);

var layout = new FA2Layout(graph, {
settings: forceAtlas2.inferSettings(graph)
});
Expand All @@ -135,6 +164,7 @@ s.on("clickNode", function(e) {

exports.show = function() {
element.style.display = "block";
legend.show(legendElement);
// Zero-sized while hidden, so re-measure before refreshing.
s.resize();
s.refresh();
Expand All @@ -143,5 +173,6 @@ exports.show = function() {

exports.hide = function() {
element.style.display = "none";
legend.hide(legendElement);
layout.stop();
};
84 changes: 84 additions & 0 deletions app/graphs/legend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// The graphs say a lot with colour, size and arrow direction, and none of it
// was written down anywhere in the UI (webpack/analyse#29).
//
// A legend is only worth having if it cannot drift away from the drawing, so
// nothing here holds a colour of its own: swatches are filled by sampling the
// very scales the graphs are drawn with, and the graphs pass in the values
// their scales run over.

// Inserts an empty legend after the graph container. Hidden until the graph it
// belongs to is shown.
exports.create = function create(graphElement) {
var element = document.createElement("div");
element.className = "graph-legend";
element.style.display = "none";
graphElement.parentNode.insertBefore(element, graphElement.nextSibling);
return element;
};

exports.show = function show(element) {
element.style.display = "";
};

exports.hide = function hide(element) {
element.style.display = "none";
};

// Samples a percentageToColor scale into the stops of a CSS gradient, so a
// swatch shows the range a value is actually drawn in rather than an
// approximation of it.
exports.gradient = function gradient(scale, steps) {
var stops = [];
steps = steps || 6;
for (var i = 0; i < steps; i++) stops.push(scale(i / (steps - 1)));
return stops;
};

// groups: [{ title, items: [{ shape, color | gradient | glyph, text }] }]
// shape is "dot" for anything drawn as a node and "line" for anything drawn as
// an edge, which is the only distinction the graphs make.
exports.render = function render(element, groups) {
element.textContent = "";
groups.forEach(function(group) {
var groupElement = document.createElement("div");
groupElement.className = "graph-legend-group";
if (group.title) {
var title = document.createElement("span");
title.className = "graph-legend-title";
title.textContent = group.title;
groupElement.appendChild(title);
}
group.items.forEach(function(item) {
groupElement.appendChild(renderItem(item));
});
element.appendChild(groupElement);
});
};

function renderItem(item) {
var element = document.createElement("span");
element.className = "graph-legend-item";
element.appendChild(renderSwatch(item));
var text = document.createElement("span");
text.textContent = item.text;
element.appendChild(text);
return element;
}

function renderSwatch(item) {
var swatch = document.createElement("span");
if (item.glyph) {
swatch.className = "graph-legend-glyph";
swatch.textContent = item.glyph;
return swatch;
}
swatch.className =
"graph-legend-swatch graph-legend-swatch-" + (item.shape || "dot");
if (item.gradient) {
swatch.style.backgroundImage =
"linear-gradient(to right," + item.gradient.join(",") + ")";
} else {
swatch.style.backgroundColor = item.color;
}
return swatch;
}
93 changes: 93 additions & 0 deletions app/graphs/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ var FA2Layout = require("graphology-layout-forceatlas2/worker");
var forceAtlas2 = require("graphology-layout-forceatlas2");
var rescale = require("./rescale");
var theme = require("./theme");
var legend = require("./legend");
var formatSize = require("../formatSize");
var percentageToColor = require("../percentageToColor").greenRed;
var percentageToColor2 = require("../percentageToColor").blue;

var element = document.getElementById("sigma-modules");
var legendElement = legend.create(element);

var nodes = [];
var edges = [];
Expand Down Expand Up @@ -145,6 +148,91 @@ edges.forEach(function(edge) {
});
});

// What the graph draws, in words. The wording follows the code above: node
// colour and radius come from the module size, edge colour from the timestamp
// of the module the edge points at, and edge width from how many modules ask
// for that same module.
var GRAPH_LEGEND = [
{
title: "module",
items: [
{
shape: "dot",
gradient: legend.gradient(percentageToColor),
text: "colour and radius: size, 0 to " + formatSize(maxSize)
}
]
},
{
title: "dependency",
items: [
{ glyph: "\u2192", text: "requires the module it points at" },
maxTimestamp > 0
? {
shape: "line",
gradient: legend.gradient(percentageToColor2),
text: "finished building, 0 to " + maxTimestamp + " ms"
}
: {
shape: "line",
color: "rgba(120,130,140,0.6)",
text: "coloured like the module it points at"
},
{
shape: "line",
color: "rgba(120,130,140,0.6)",
text:
"thinner when many modules require the same one, and when it " +
"is loaded from an async chunk"
}
]
}
];

// The roles a module selection paints. A chunk selection paints a different
// set, so the two are kept apart rather than listing every colour the graph
// could ever use next to a view that cannot show most of them.
var MODULE_SELECTION_LEGEND = {
title: "selected module",
items: [
{ shape: "dot", color: theme.ROLE_COLOR.active, text: "the module" },
{ shape: "dot", color: theme.ROLE_COLOR.reason, text: "requires it" },
{
shape: "dot",
color: theme.ROLE_COLOR.dependency,
text: "required by it"
},
{ shape: "dot", color: theme.FADED_NODE_COLOR, text: "unrelated" }
]
};

var CHUNK_SELECTION_LEGEND = {
title: "selected chunk",
items: [
{ shape: "dot", color: theme.ROLE_COLOR.member, text: "in the chunk" },
{
shape: "line",
color: theme.ROLE_COLOR.dependency,
text: "requires a module outside it"
},
{
shape: "line",
color: theme.ROLE_COLOR.reason,
text: "required from outside it"
},
{ shape: "dot", color: theme.FADED_NODE_COLOR, text: "outside the chunk" }
]
};

function showLegend(selectionGroup) {
legend.render(
legendElement,
selectionGroup ? GRAPH_LEGEND.concat(selectionGroup) : GRAPH_LEGEND
);
}

showLegend(null);

// null when nothing is selected. Otherwise { nodes: {key: role}, edges: {key:
// role} }. Selection drives appearance through the reducers rather than by
// overwriting colour attributes, so clearing a selection needs no restore pass
Expand Down Expand Up @@ -271,6 +359,7 @@ s.on("clickNode", function(e) {

exports.show = function() {
element.style.display = "block";
legend.show(legendElement);
// The container has zero dimensions while hidden, so re-measure before
// refreshing or sigma keeps the stale 0x0 viewport.
s.resize();
Expand All @@ -280,12 +369,14 @@ exports.show = function() {

exports.hide = function() {
element.style.display = "none";
legend.hide(legendElement);
layout.stop();
};

exports.setNormal = function() {
activeModuleUid = null;
selection = null;
showLegend(null);
s.refresh();
};

Expand Down Expand Up @@ -316,6 +407,7 @@ exports.setActiveModule = function(activeModule) {
edgeRoles[edge] = "dependency";
});
selection = buildSelection(nodeRoles, edgeRoles);
showLegend(MODULE_SELECTION_LEGEND);
s.refresh();
};

Expand All @@ -337,5 +429,6 @@ exports.setActiveChunk = function(activeChunk) {
else if (tc) edgeRoles[edge] = "reason";
});
selection = buildSelection(nodeRoles, edgeRoles);
showLegend(CHUNK_SELECTION_LEGEND);
s.refresh();
};
54 changes: 54 additions & 0 deletions app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,57 @@ table pre {
.sortable-th {
cursor: pointer;
}

/* Legend under the module and chunk graphs (webpack/analyse#29). Sits between
the graph and the page content, so it stays quiet: one line of small text
that reads as a caption rather than as another table. */
.graph-legend {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 18px;
margin: 0 0 10px 8px;
color: #777;
font-size: 12px;
line-height: 18px;
}
.graph-legend-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 10px;
}
.graph-legend-title {
color: #555;
font-weight: bold;
}
.graph-legend-title:after {
content: ":";
}
.graph-legend-item {
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.graph-legend-swatch {
display: inline-block;
border: 1px solid rgba(0, 0, 0, 0.12);
}
.graph-legend-swatch-dot {
width: 26px;
height: 10px;
border-radius: 5px;
}
.graph-legend-swatch-line {
width: 26px;
height: 4px;
border-radius: 2px;
}
.graph-legend-glyph {
display: inline-block;
width: 26px;
color: #555;
text-align: center;
font-size: 14px;
}