summaryrefslogtreecommitdiffstats
path: root/devtools/client/memory/components/tree-map/draw.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/memory/components/tree-map/draw.js')
-rw-r--r--devtools/client/memory/components/tree-map/draw.js295
1 files changed, 295 insertions, 0 deletions
diff --git a/devtools/client/memory/components/tree-map/draw.js b/devtools/client/memory/components/tree-map/draw.js
new file mode 100644
index 000000000..a55c1eae6
--- /dev/null
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+/**
+ * Draw the treemap into the provided canvases using the 2d context. The treemap
+ * layout is computed with d3. There are 2 canvases provided, each matching
+ * the resolution of the window. The main canvas is a fully drawn version of
+ * the treemap that is positioned and zoomed using css. It gets blurry the more
+ * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
+ * repositioned absolutely after every change in the dragZoom object, and then
+ * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
+ * of the treemap.
+ */
+
+const colorCoarseType = require("./color-coarse-type");
+const {
+ hslToStyle,
+ formatAbbreviatedBytes,
+ L10N
+} = require("devtools/client/memory/utils");
+
+// A constant fully zoomed out dragZoom object for the main canvas
+const NO_SCROLL = {
+ translateX: 0,
+ translateY: 0,
+ zoom: 0,
+ offsetX: 0,
+ offsetY: 0
+};
+
+// Drawing constants
+const ELLIPSIS = "...";
+const TEXT_MARGIN = 2;
+const TEXT_COLOR = "#000000";
+const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
+const LINE_WIDTH = 1;
+const FONT_SIZE = 10;
+const FONT_LINE_HEIGHT = 2;
+const PADDING = [5 + FONT_SIZE, 5, 5, 5];
+const COUNT_LABEL = L10N.getStr("tree-map.node-count");
+
+/**
+ * Setup and start drawing the treemap visualization
+ *
+ * @param {Object} report
+ * @param {Object} canvases
+ * A CanvasUtils object that contains references to the main and zoom
+ * canvases and contexts
+ * @param {Object} dragZoom
+ * A DragZoom object representing the current state of the dragging
+ * and zooming behavior
+ */
+exports.setupDraw = function (report, canvases, dragZoom) {
+ let getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
+
+ let treemap, nodes;
+
+ function drawFullTreemap() {
+ treemap = getTreemap();
+ nodes = treemap(report);
+ drawTreemap(canvases.main, nodes, NO_SCROLL);
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ }
+
+ function drawZoomedTreemap() {
+ drawTreemap(canvases.zoom, nodes, dragZoom);
+ positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
+ }
+
+ drawFullTreemap();
+ canvases.on("resize", drawFullTreemap);
+ dragZoom.on("change", drawZoomedTreemap);
+};
+
+/**
+ * Returns a configured d3 treemap function
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @return {Function}
+ */
+const configureD3Treemap = exports.configureD3Treemap = function (canvas) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let treemap = window.d3.layout.treemap()
+ .size([
+ // The d3 layout includes the padding around everything, add some
+ // extra padding to the size to compensate for thi
+ canvas.width + (PADDING[1] + PADDING[3]) * ratio,
+ canvas.height + (PADDING[0] + PADDING[2]) * ratio
+ ])
+ .sticky(true)
+ .padding([
+ PADDING[0] * ratio,
+ PADDING[1] * ratio,
+ PADDING[2] * ratio,
+ PADDING[3] * ratio,
+ ])
+ .value(d => d.bytes);
+
+ /**
+ * Create treemap nodes from a census report that are sorted by depth
+ *
+ * @param {Object} report
+ * @return {Array} An array of d3 treemap nodes
+ * // https://github.com/mbostock/d3/wiki/Treemap-Layout
+ * parent - the parent node, or null for the root.
+ * children - the array of child nodes, or null for leaf nodes.
+ * value - the node value, as returned by the value accessor.
+ * depth - the depth of the node, starting at 0 for the root.
+ * area - the computed pixel area of this node.
+ * x - the minimum x-coordinate of the node position.
+ * y - the minimum y-coordinate of the node position.
+ * z - the orientation of this cell’s subdivision, if any.
+ * dx - the x-extent of the node position.
+ * dy - the y-extent of the node position.
+ */
+ return function depthSortedNodes(report) {
+ let nodes = treemap(report);
+ nodes.sort((a, b) => a.depth - b.depth);
+ return nodes;
+ };
+};
+
+/**
+ * Draw the text, cut it in half every time it doesn't fit until it fits or
+ * it's smaller than the "..." text.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * the position of the text
+ * @param {Number} y
+ * the position of the text
+ * @param {Number} innerWidth
+ * the inner width of the containing treemap cell
+ * @param {Text} name
+ */
+const drawTruncatedName = exports.drawTruncatedName = function (ctx, x, y,
+ innerWidth,
+ name) {
+ let truncated = name.substr(0, Math.floor(name.length / 2));
+ let formatted = truncated + ELLIPSIS;
+
+ if (ctx.measureText(formatted).width > innerWidth) {
+ drawTruncatedName(ctx, x, y, innerWidth, truncated);
+ } else {
+ ctx.fillText(formatted, x, y);
+ }
+};
+
+/**
+ * Fit and draw the text in a node with the following strategies to shrink
+ * down the text size:
+ *
+ * Function 608KB 9083 count
+ * Function
+ * Func...
+ * Fu...
+ * ...
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawText = exports.drawText = function (ctx, node, borderWidth, ratio,
+ dragZoom, padding) {
+ let { dx, dy, name, totalBytes, totalCount } = node;
+ let scale = dragZoom.zoom + 1;
+ dx *= scale;
+ dy *= scale;
+
+ // Start checking to see how much text we can fit in, optimizing for the
+ // common case of lots of small leaf nodes
+ if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
+ let margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
+ let x = margin + (node.x - padding[0]) * scale - dragZoom.offsetX;
+ let y = margin + (node.y - padding[1]) * scale - dragZoom.offsetY;
+ let innerWidth = dx - margin * 2;
+ let nameSize = ctx.measureText(name).width;
+
+ if (ctx.measureText(ELLIPSIS).width > innerWidth) {
+ return;
+ }
+
+ ctx.fillStyle = TEXT_COLOR;
+
+ if (nameSize > innerWidth) {
+ // The name is too long - halve the name as an expediant way to shorten it
+ drawTruncatedName(ctx, x, y, innerWidth, name);
+ } else {
+ let bytesFormatted = formatAbbreviatedBytes(totalBytes);
+ let countFormatted = `${totalCount} ${COUNT_LABEL}`;
+ let byteSize = ctx.measureText(bytesFormatted).width;
+ let countSize = ctx.measureText(countFormatted).width;
+ let spaceSize = ctx.measureText(" ").width;
+
+ if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
+ // The full name will fit
+ ctx.fillText(`${name}`, x, y);
+ } else {
+ // The full name plus the byte information will fit
+ ctx.fillText(name, x, y);
+ ctx.fillStyle = TEXT_LIGHT_COLOR;
+ ctx.fillText(`${bytesFormatted} ${countFormatted}`,
+ x + nameSize + spaceSize, y);
+ }
+ }
+ }
+};
+
+/**
+ * Draw a box given a node
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Object} node
+ * @param {Number} borderWidth
+ * @param {Number} ratio
+ * @param {Object} dragZoom
+ * @param {Array} padding
+ */
+const drawBox = exports.drawBox = function (ctx, node, borderWidth, dragZoom,
+ padding) {
+ let border = borderWidth(node);
+ let fillHSL = colorCoarseType(node);
+ let strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
+ let scale = 1 + dragZoom.zoom;
+
+ // Offset the draw so that box strokes don't overlap
+ let x = scale * (node.x - padding[0]) - dragZoom.offsetX + border / 2;
+ let y = scale * (node.y - padding[1]) - dragZoom.offsetY + border / 2;
+ let dx = scale * node.dx - border;
+ let dy = scale * node.dy - border;
+
+ ctx.fillStyle = hslToStyle(...fillHSL);
+ ctx.fillRect(x, y, dx, dy);
+
+ ctx.strokeStyle = hslToStyle(...strokeHSL);
+ ctx.lineWidth = border;
+ ctx.strokeRect(x, y, dx, dy);
+};
+
+/**
+ * Draw the overall treemap
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Array} nodes
+ * @param {Objbect} dragZoom
+ */
+const drawTreemap = exports.drawTreemap = function ({canvas, ctx}, nodes,
+ dragZoom) {
+ let window = canvas.ownerDocument.defaultView;
+ let ratio = window.devicePixelRatio;
+ let canvasArea = canvas.width * canvas.height;
+ // Subtract the outer padding from the tree map layout.
+ let padding = [PADDING[3] * ratio, PADDING[0] * ratio];
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
+ ctx.textBaseline = "top";
+
+ function borderWidth(node) {
+ let areaRatio = Math.sqrt(node.area / canvasArea);
+ return ratio * Math.max(1, LINE_WIDTH * areaRatio);
+ }
+
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+ if (node.parent === undefined) {
+ continue;
+ }
+
+ drawBox(ctx, node, borderWidth, dragZoom, padding);
+ drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+ }
+};
+
+/**
+ * Set the position of the zoomed in canvas. It always take up 100% of the view
+ * window, but is transformed relative to the zoomed in containing element,
+ * essentially reversing the transform of the containing element.
+ *
+ * @param {HTMLCanvasElement} canvas
+ * @param {Object} dragZoom
+ */
+const positionZoomedCanvas = function (canvas, dragZoom) {
+ let scale = 1 / (1 + dragZoom.zoom);
+ let x = -dragZoom.translateX;
+ let y = -dragZoom.translateY;
+ canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
+};
+
+exports.positionZoomedCanvas = positionZoomedCanvas;