diff options
Diffstat (limited to 'devtools/client/memory/components/tree-map')
6 files changed, 859 insertions, 0 deletions
diff --git a/devtools/client/memory/components/tree-map/canvas-utils.js b/devtools/client/memory/components/tree-map/canvas-utils.js new file mode 100644 index 000000000..c7d67a0bf --- /dev/null +++ b/devtools/client/memory/components/tree-map/canvas-utils.js @@ -0,0 +1,134 @@ +/* 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/. */ + +/* eslint-env browser */ + +"use strict"; + +/** + * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom + * canvas. The main canvas dimensions match the parent div, but the CSS can be + * transformed to be zoomed and dragged around (potentially creating a blurry + * canvas once zoomed in). The zoom canvas is a zoomed in section that matches + * the parent div's dimensions and is kept in place through CSS. A zoomed in + * view of the visualization is drawn onto this canvas, providing a crisp zoomed + * in view of the tree map. + */ +const { debounce } = require("sdk/lang/functional"); +const EventEmitter = require("devtools/shared/event-emitter"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const FULLSCREEN_STYLE = { + width: "100%", + height: "100%", + position: "absolute", +}; + +/** + * Create the canvases, resize handlers, and return references to them all + * + * @param {HTMLDivElement} parentEl + * @param {Number} debounceRate + * @return {Object} + */ +function Canvases(parentEl, debounceRate) { + EventEmitter.decorate(this); + this.container = createContainingDiv(parentEl); + + // This canvas contains all of the treemap + this.main = createCanvas(this.container, "main"); + // This canvas contains only the zoomed in portion, overlaying the main canvas + this.zoom = createCanvas(this.container, "zoom"); + + this.removeHandlers = handleResizes(this, debounceRate); +} + +Canvases.prototype = { + + /** + * Remove the handlers and elements + * + * @return {type} description + */ + destroy : function () { + this.removeHandlers(); + this.container.removeChild(this.main.canvas); + this.container.removeChild(this.zoom.canvas); + } +}; + +module.exports = Canvases; + +/** + * Create the containing div + * + * @param {HTMLDivElement} parentEl + * @return {HTMLDivElement} + */ +function createContainingDiv(parentEl) { + let div = parentEl.ownerDocument.createElementNS(HTML_NS, "div"); + Object.assign(div.style, FULLSCREEN_STYLE); + parentEl.appendChild(div); + return div; +} + +/** + * Create a canvas and context + * + * @param {HTMLDivElement} container + * @param {String} className + * @return {Object} { canvas, ctx } + */ +function createCanvas(container, className) { + let window = container.ownerDocument.defaultView; + let canvas = container.ownerDocument.createElementNS(HTML_NS, "canvas"); + container.appendChild(canvas); + canvas.width = container.offsetWidth * window.devicePixelRatio; + canvas.height = container.offsetHeight * window.devicePixelRatio; + canvas.className = className; + + Object.assign(canvas.style, FULLSCREEN_STYLE, { + pointerEvents: "none" + }); + + let ctx = canvas.getContext("2d"); + + return { canvas, ctx }; +} + +/** + * Resize the canvases' resolutions, and fires out the onResize callback + * + * @param {HTMLDivElement} container + * @param {Object} canvases + * @param {Number} debounceRate + */ +function handleResizes(canvases, debounceRate) { + let { container, main, zoom } = canvases; + let window = container.ownerDocument.defaultView; + + function resize() { + let width = container.offsetWidth * window.devicePixelRatio; + let height = container.offsetHeight * window.devicePixelRatio; + + main.canvas.width = width; + main.canvas.height = height; + zoom.canvas.width = width; + zoom.canvas.height = height; + + canvases.emit("resize"); + } + + // Tests may not need debouncing + let debouncedResize = debounceRate > 0 + ? debounce(resize, debounceRate) + : resize; + + window.addEventListener("resize", debouncedResize, false); + resize(); + + return function removeResizeHandlers() { + window.removeEventListener("resize", debouncedResize, false); + }; +} diff --git a/devtools/client/memory/components/tree-map/color-coarse-type.js b/devtools/client/memory/components/tree-map/color-coarse-type.js new file mode 100644 index 000000000..5f033ea26 --- /dev/null +++ b/devtools/client/memory/components/tree-map/color-coarse-type.js @@ -0,0 +1,70 @@ +/* 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"; + +/** + * Color the boxes in the treemap + */ + +const TYPES = [ "objects", "other", "strings", "scripts" ]; + +// The factors determine how much the hue shifts +const TYPE_FACTOR = TYPES.length * 3; +const DEPTH_FACTOR = -10; +const H = 0.5; +const S = 0.6; +const L = 0.9; + +/** + * Recursively find the index of the coarse type of a node + * + * @param {Object} node + * d3 treemap + * @return {Integer} + * index + */ +function findCoarseTypeIndex(node) { + let index = TYPES.indexOf(node.name); + + if (node.parent) { + return index === -1 ? findCoarseTypeIndex(node.parent) : index; + } + + return TYPES.indexOf("other"); +} + +/** + * Decide a color value for depth to be used in the HSL computation + * + * @param {Object} node + * @return {Number} + */ +function depthColorFactor(node) { + return Math.min(1, node.depth / DEPTH_FACTOR); +} + +/** + * Decide a color value for type to be used in the HSL computation + * + * @param {Object} node + * @return {Number} + */ +function typeColorFactor(node) { + return findCoarseTypeIndex(node) / TYPE_FACTOR; +} + +/** + * Color a node + * + * @param {Object} node + * @return {Array} HSL values ranged 0-1 + */ +module.exports = function colorCoarseType(node) { + let h = Math.min(1, H + typeColorFactor(node)); + let s = Math.min(1, S); + let l = Math.min(1, L + depthColorFactor(node)); + + return [h, s, l]; +}; diff --git a/devtools/client/memory/components/tree-map/drag-zoom.js b/devtools/client/memory/components/tree-map/drag-zoom.js new file mode 100644 index 000000000..3de970725 --- /dev/null +++ b/devtools/client/memory/components/tree-map/drag-zoom.js @@ -0,0 +1,316 @@ +/* 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"; + +const { debounce } = require("sdk/lang/functional"); +const { lerp } = require("devtools/client/memory/utils"); +const EventEmitter = require("devtools/shared/event-emitter"); + +const LERP_SPEED = 0.5; +const ZOOM_SPEED = 0.01; +const TRANSLATE_EPSILON = 1; +const ZOOM_EPSILON = 0.001; +const LINE_SCROLL_MODE = 1; +const SCROLL_LINE_SIZE = 15; + +/** + * DragZoom is a constructor that contains the state of the current dragging and + * zooming behavior. It sets the scrolling and zooming behaviors. + * + * @param {HTMLElement} container description + * The container for the canvases + */ +function DragZoom(container, debounceRate, requestAnimationFrame) { + EventEmitter.decorate(this); + + this.isDragging = false; + + // The current mouse position + this.mouseX = container.offsetWidth / 2; + this.mouseY = container.offsetHeight / 2; + + // The total size of the visualization after being zoomed, in pixels + this.zoomedWidth = container.offsetWidth; + this.zoomedHeight = container.offsetHeight; + + // How much the visualization has been zoomed in + this.zoom = 0; + + // The offset of visualization from the container. This is applied after + // the zoom, and the visualization by default is centered + this.translateX = 0; + this.translateY = 0; + + // The size of the offset between the top/left of the container, and the + // top/left of the containing element. This value takes into account + // the devicePixelRatio for canvas draws. + this.offsetX = 0; + this.offsetY = 0; + + // The smoothed values that are animated and eventually match the target + // values. The values are updated by the update loop + this.smoothZoom = 0; + this.smoothTranslateX = 0; + this.smoothTranslateY = 0; + + // Add the constant values for testing purposes + this.ZOOM_SPEED = ZOOM_SPEED; + this.ZOOM_EPSILON = ZOOM_EPSILON; + + let update = createUpdateLoop(container, this, requestAnimationFrame); + + this.destroy = setHandlers(this, container, update, debounceRate); +} + +module.exports = DragZoom; + +/** + * Returns an update loop. This loop smoothly updates the visualization when + * actions are performed. Once the animations have reached their target values + * the animation loop is stopped. + * + * Any value in the `dragZoom` object that starts with "smooth" is the + * smoothed version of a value that is interpolating toward the target value. + * For instance `dragZoom.smoothZoom` approaches `dragZoom.zoom` on each + * iteration of the update loop until it's sufficiently close as defined by + * the epsilon values. + * + * Only these smoothed values and the container CSS are updated by the loop. + * + * @param {HTMLDivElement} container + * @param {Object} dragZoom + * The values that represent the current dragZoom state + * @param {Function} requestAnimationFrame + */ +function createUpdateLoop(container, dragZoom, requestAnimationFrame) { + let isLooping = false; + + function update() { + let isScrollChanging = ( + Math.abs(dragZoom.smoothZoom - dragZoom.zoom) > ZOOM_EPSILON + ); + let isTranslateChanging = ( + Math.abs(dragZoom.smoothTranslateX - dragZoom.translateX) + > TRANSLATE_EPSILON || + Math.abs(dragZoom.smoothTranslateY - dragZoom.translateY) + > TRANSLATE_EPSILON + ); + + isLooping = isScrollChanging || isTranslateChanging; + + if (isScrollChanging) { + dragZoom.smoothZoom = lerp(dragZoom.smoothZoom, dragZoom.zoom, + LERP_SPEED); + } else { + dragZoom.smoothZoom = dragZoom.zoom; + } + + if (isTranslateChanging) { + dragZoom.smoothTranslateX = lerp(dragZoom.smoothTranslateX, + dragZoom.translateX, LERP_SPEED); + dragZoom.smoothTranslateY = lerp(dragZoom.smoothTranslateY, + dragZoom.translateY, LERP_SPEED); + } else { + dragZoom.smoothTranslateX = dragZoom.translateX; + dragZoom.smoothTranslateY = dragZoom.translateY; + } + + let zoom = 1 + dragZoom.smoothZoom; + let x = dragZoom.smoothTranslateX; + let y = dragZoom.smoothTranslateY; + container.style.transform = `translate(${x}px, ${y}px) scale(${zoom})`; + + if (isLooping) { + requestAnimationFrame(update); + } + } + + // Go ahead and start the update loop + update(); + + return function restartLoopingIfStopped() { + if (!isLooping) { + update(); + } + }; +} + +/** + * Set the various event listeners and return a function to remove them + * + * @param {Object} dragZoom + * @param {HTMLElement} container + * @param {Function} update + * @return {Function} The function to remove the handlers + */ +function setHandlers(dragZoom, container, update, debounceRate) { + let emitChanged = debounce(() => dragZoom.emit("change"), debounceRate); + + let removeDragHandlers = + setDragHandlers(container, dragZoom, emitChanged, update); + let removeScrollHandlers = + setScrollHandlers(container, dragZoom, emitChanged, update); + + return function removeHandlers() { + removeDragHandlers(); + removeScrollHandlers(); + }; +} + +/** + * Sets handlers for when the user drags on the canvas. It will update dragZoom + * object with new translate and offset values. + * + * @param {HTMLElement} container + * @param {Object} dragZoom + * @param {Function} changed + * @param {Function} update + */ +function setDragHandlers(container, dragZoom, emitChanged, update) { + let parentEl = container.parentElement; + + function startDrag() { + dragZoom.isDragging = true; + container.style.cursor = "grabbing"; + } + + function stopDrag() { + dragZoom.isDragging = false; + container.style.cursor = "grab"; + } + + function drag(event) { + let prevMouseX = dragZoom.mouseX; + let prevMouseY = dragZoom.mouseY; + + dragZoom.mouseX = event.clientX - parentEl.offsetLeft; + dragZoom.mouseY = event.clientY - parentEl.offsetTop; + + if (!dragZoom.isDragging) { + return; + } + + dragZoom.translateX += dragZoom.mouseX - prevMouseX; + dragZoom.translateY += dragZoom.mouseY - prevMouseY; + + keepInView(container, dragZoom); + + emitChanged(); + update(); + } + + parentEl.addEventListener("mousedown", startDrag, false); + parentEl.addEventListener("mouseup", stopDrag, false); + parentEl.addEventListener("mouseout", stopDrag, false); + parentEl.addEventListener("mousemove", drag, false); + + return function removeListeners() { + parentEl.removeEventListener("mousedown", startDrag, false); + parentEl.removeEventListener("mouseup", stopDrag, false); + parentEl.removeEventListener("mouseout", stopDrag, false); + parentEl.removeEventListener("mousemove", drag, false); + }; +} + +/** + * Sets the handlers for when the user scrolls. It updates the dragZoom object + * and keeps the canvases all within the view. After changing values update + * loop is called, and the changed event is emitted. + * + * @param {HTMLDivElement} container + * @param {Object} dragZoom + * @param {Function} changed + * @param {Function} update + */ +function setScrollHandlers(container, dragZoom, emitChanged, update) { + let window = container.ownerDocument.defaultView; + + function handleWheel(event) { + event.preventDefault(); + + if (dragZoom.isDragging) { + return; + } + + // Update the zoom level + let scrollDelta = getScrollDelta(event, window); + let prevZoom = dragZoom.zoom; + dragZoom.zoom = Math.max(0, dragZoom.zoom - scrollDelta * ZOOM_SPEED); + let deltaZoom = dragZoom.zoom - prevZoom; + + // Calculate the updated width and height + let prevZoomedWidth = container.offsetWidth * (1 + prevZoom); + let prevZoomedHeight = container.offsetHeight * (1 + prevZoom); + dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom); + dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom); + let deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth; + let deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight; + + let mouseOffsetX = dragZoom.mouseX - container.offsetWidth / 2; + let mouseOffsetY = dragZoom.mouseY - container.offsetHeight / 2; + + // The ratio of where the center of the mouse is in regards to the total + // zoomed width/height + let ratioZoomX = (prevZoomedWidth / 2 + mouseOffsetX - dragZoom.translateX) + / prevZoomedWidth; + let ratioZoomY = (prevZoomedHeight / 2 + mouseOffsetY - dragZoom.translateY) + / prevZoomedHeight; + + // Distribute the change in width and height based on the above ratio + dragZoom.translateX -= lerp(-deltaWidth / 2, deltaWidth / 2, ratioZoomX); + dragZoom.translateY -= lerp(-deltaHeight / 2, deltaHeight / 2, ratioZoomY); + + // Keep the canvas in range of the container + keepInView(container, dragZoom); + emitChanged(); + update(); + } + + container.addEventListener("wheel", handleWheel, false); + + return function removeListener() { + container.removeEventListener("wheel", handleWheel, false); + }; +} + +/** + * Account for the various mouse wheel event types, per pixel or per line + * + * @param {WheelEvent} event + * @param {Window} window + * @return {Number} The scroll size in pixels + */ +function getScrollDelta(event, window) { + if (event.deltaMode === LINE_SCROLL_MODE) { + // Update by a fixed arbitrary value to normalize scroll types + return event.deltaY * SCROLL_LINE_SIZE; + } + return event.deltaY; +} + +/** + * Keep the dragging and zooming within the view by updating the values in the + * `dragZoom` object. + * + * @param {HTMLDivElement} container + * @param {Object} dragZoom + */ +function keepInView(container, dragZoom) { + let { devicePixelRatio } = container.ownerDocument.defaultView; + let overdrawX = (dragZoom.zoomedWidth - container.offsetWidth) / 2; + let overdrawY = (dragZoom.zoomedHeight - container.offsetHeight) / 2; + + dragZoom.translateX = Math.max(-overdrawX, + Math.min(overdrawX, dragZoom.translateX)); + dragZoom.translateY = Math.max(-overdrawY, + Math.min(overdrawY, dragZoom.translateY)); + + dragZoom.offsetX = devicePixelRatio * ( + (dragZoom.zoomedWidth - container.offsetWidth) / 2 - dragZoom.translateX + ); + dragZoom.offsetY = devicePixelRatio * ( + (dragZoom.zoomedHeight - container.offsetHeight) / 2 - dragZoom.translateY + ); +} 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; diff --git a/devtools/client/memory/components/tree-map/moz.build b/devtools/client/memory/components/tree-map/moz.build new file mode 100644 index 000000000..aab193191 --- /dev/null +++ b/devtools/client/memory/components/tree-map/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'canvas-utils.js', + 'color-coarse-type.js', + 'drag-zoom.js', + 'draw.js', + 'start.js', +) diff --git a/devtools/client/memory/components/tree-map/start.js b/devtools/client/memory/components/tree-map/start.js new file mode 100644 index 000000000..9cebe2a9d --- /dev/null +++ b/devtools/client/memory/components/tree-map/start.js @@ -0,0 +1,32 @@ +/* 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"; + +const { setupDraw } = require("./draw"); +const DragZoom = require("./drag-zoom"); +const CanvasUtils = require("./canvas-utils"); + +/** + * Start the tree map visualization + * + * @param {HTMLDivElement} container + * @param {Object} report + * the report from a census + * @param {Number} debounceRate + */ +module.exports = function startVisualization(parentEl, report, + debounceRate = 60) { + let window = parentEl.ownerDocument.defaultView; + let canvases = new CanvasUtils(parentEl, debounceRate); + let dragZoom = new DragZoom(canvases.container, debounceRate, + window.requestAnimationFrame); + + setupDraw(report, canvases, dragZoom); + + return function stopVisualization() { + canvases.destroy(); + dragZoom.destroy(); + }; +}; |