mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-18 02:13:08 +09:00
295 lines
9.6 KiB
JavaScript
295 lines
9.6 KiB
JavaScript
|
|
/* 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;
|