Bug 1261159 - Tree map zooming is awkward. r=fitzgen
authorGreg Tatum <tatum.creative@gmail.com>
Thu, 07 Apr 2016 07:27:00 -0400
changeset 316029 50ed998729df3a0cb678628736eb7dc7993d7520
parent 316028 4a759751328040c59732fc91f8423da7d2db1382
child 316030 48fac32381ec9ae8f0041d83441b3a347004e0d7
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfitzgen
bugs1261159
milestone48.0a1
Bug 1261159 - Tree map zooming is awkward. r=fitzgen
devtools/client/memory/components/tree-map/drag-zoom.js
devtools/client/memory/components/tree-map/draw.js
devtools/client/memory/components/tree-map/start.js
devtools/client/memory/test/unit/test_tree-map-01.js
devtools/client/memory/test/unit/test_tree-map-02.js
devtools/client/themes/memory.css
--- a/devtools/client/memory/components/tree-map/drag-zoom.js
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -236,51 +236,37 @@ function setScrollHandlers(container, dr
 
     // 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 prevWidth = container.offsetWidth * (1 + prevZoom);
-    let prevHeight = container.offsetHeight * (1 + prevZoom);
+    let prevZoomedWidth = container.offsetWidth * (1 + prevZoom);
+    let prevZoomedHeight = container.offsetHeight * (1 + prevZoom);
     dragZoom.zoomedWidth = container.offsetWidth * (1 + dragZoom.zoom);
-    dragZoom.height = container.offsetHeight * (1 + dragZoom.zoom);
-    let deltaWidth = dragZoom.zoomedWidth - prevWidth;
-    let deltaHeight = dragZoom.height - prevHeight;
+    dragZoom.zoomedHeight = container.offsetHeight * (1 + dragZoom.zoom);
+    let deltaWidth = dragZoom.zoomedWidth - prevZoomedWidth;
+    let deltaHeight = dragZoom.zoomedHeight - prevZoomedHeight;
 
-    // The ratio of where the center of the zoom is in regards to the total
+    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 = (dragZoom.zoomedWidth / 2 - dragZoom.translateX)
+    let ratioZoomX = (dragZoom.zoomedWidth / 2 + mouseOffsetX - dragZoom.translateX)
       / dragZoom.zoomedWidth;
-    let ratioZoomY = (dragZoom.height / 2 - dragZoom.translateY)
-      / dragZoom.height;
+    let ratioZoomY = (dragZoom.zoomedHeight / 2 + mouseOffsetY - dragZoom.translateY)
+      / dragZoom.zoomedHeight;
 
     // 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);
 
-    // The ratio of mouse position to total zoomeed width/height, ranged [-1, 1]
-    let mouseRatioX, mouseRatioY;
-    if (deltaZoom > 0) {
-      // Zoom in towards the mouse
-      mouseRatioX = 2 * (dragZoom.mouseX - container.offsetWidth / 2)
-        / dragZoom.zoomedWidth;
-      mouseRatioY = 2 * (dragZoom.mouseY - container.offsetHeight / 2)
-        / dragZoom.height;
-    } else {
-      // Zoom out centering the screen
-      mouseRatioX = 0;
-      mouseRatioY = 0;
-    }
-    // Adjust the translate to zoom towards the mouse
-    dragZoom.translateX -= deltaWidth * mouseRatioX;
-    dragZoom.translateY -= deltaHeight * mouseRatioY;
-
     // Keep the canvas in range of the container
     keepInView(container, dragZoom);
     emitChanged();
     update();
   }
 
   window.addEventListener("wheel", handleWheel, false);
 
@@ -309,22 +295,22 @@ function getScrollDelta(event, window) {
  * `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.height - container.offsetHeight) / 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.height - container.offsetHeight) / 2 - dragZoom.translateY
+    (dragZoom.zoomedHeight - container.offsetHeight) / 2 - dragZoom.translateY
   );
 }
--- a/devtools/client/memory/components/tree-map/draw.js
+++ b/devtools/client/memory/components/tree-map/draw.js
@@ -32,17 +32,17 @@ const NO_SCROLL = {
 // 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, 5, 5, 5];
+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
@@ -78,20 +78,25 @@ exports.setupDraw = function(report, can
  *
  * @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([canvas.width, canvas.height])
+    .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] + FONT_SIZE) * ratio,
+      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
@@ -151,32 +156,32 @@ const drawTruncatedName = exports.drawTr
  * Function
  * Func...
  * Fu...
  * ...
  *
  * @param  {CanvasRenderingContext2D} ctx
  * @param  {Object} node
  * @param  {Number} borderWidth
- * @param  {Number} ratio
  * @param  {Object} dragZoom
+ * @param  {Array}  padding
  */
 const drawText = exports.drawText = function(ctx, node, borderWidth, ratio,
-                                              dragZoom) {
+                                              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 * scale - dragZoom.offsetX;
-    let y = margin + node.y * scale - dragZoom.offsetY;
+    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;
@@ -208,26 +213,28 @@ const drawText = exports.drawText = func
 /**
  * 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) {
+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 - dragZoom.offsetX + border / 2;
-  let y = scale * node.y - dragZoom.offsetY + border / 2;
+  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;
@@ -242,33 +249,36 @@ const drawBox = exports.drawBox = functi
  * @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);
-    drawText(ctx, node, borderWidth, ratio, dragZoom);
+    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.
  *
--- a/devtools/client/memory/components/tree-map/start.js
+++ b/devtools/client/memory/components/tree-map/start.js
@@ -12,17 +12,17 @@ const CanvasUtils = require("./canvas-ut
  * 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 = 100) {
+                                              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() {
--- a/devtools/client/memory/test/unit/test_tree-map-01.js
+++ b/devtools/client/memory/test/unit/test_tree-map-01.js
@@ -16,42 +16,42 @@ add_task(function *() {
   let node = {
     x: 20,
     y: 30,
     dx: 50,
     dy: 70,
     type: "other",
     depth: 2
   };
+  let padding = [10, 10];
   let borderWidth = () => 1;
   let dragZoom = {
     offsetX: 0,
     offsetY: 0,
     zoom: 0
   };
-
-  drawBox(ctx, node, borderWidth, dragZoom);
+  drawBox(ctx, node, borderWidth, dragZoom, padding);
   ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
   equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set");
   equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set");
   equal(ctx.lineWidth, 1, "The lineWidth is set");
-  deepEqual(fillRectValues, [20.5,30.5,49,69], "Draws a filled rectangle");
-  deepEqual(strokeRectValues, [20.5,30.5,49,69], "Draws a stroked rectangle");
+  deepEqual(fillRectValues, [10.5,20.5,49,69], "Draws a filled rectangle");
+  deepEqual(strokeRectValues, [10.5,20.5,49,69], "Draws a stroked rectangle");
 
 
   dragZoom.zoom = 0.5;
 
-  drawBox(ctx, node, borderWidth, dragZoom);
+  drawBox(ctx, node, borderWidth, dragZoom, padding);
   ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues]));
-  deepEqual(fillRectValues, [30.5,45.5,74,104],
+  deepEqual(fillRectValues, [15.5,30.5,74,104],
     "Draws a zoomed filled rectangle");
-  deepEqual(strokeRectValues, [30.5,45.5,74,104],
+  deepEqual(strokeRectValues, [15.5,30.5,74,104],
     "Draws a zoomed stroked rectangle");
 
   dragZoom.offsetX = 110;
   dragZoom.offsetY = 130;
 
-  drawBox(ctx, node, borderWidth, dragZoom);
-  deepEqual(fillRectValues, [-79.5,-84.5,74,104],
+  drawBox(ctx, node, borderWidth, dragZoom, padding);
+  deepEqual(fillRectValues, [-94.5,-99.5,74,104],
     "Draws a zoomed and offset filled rectangle");
-  deepEqual(strokeRectValues, [-79.5,-84.5,74,104],
+  deepEqual(strokeRectValues, [-94.5,-99.5,74,104],
     "Draws a zoomed and offset stroked rectangle");
 });
--- a/devtools/client/memory/test/unit/test_tree-map-02.js
+++ b/devtools/client/memory/test/unit/test_tree-map-02.js
@@ -30,51 +30,52 @@ add_task(function*() {
   let ratio = 0;
   let borderWidth = () => 1;
   let dragZoom = {
     offsetX: 0,
     offsetY: 0,
     zoom: 0
   };
   let fillTextValues = [];
+  let padding = [10, 10];
 
-  drawText(ctx, node, borderWidth, ratio, dragZoom);
-  deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+  drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
+  deepEqual(fillTextValues[0], ["Example Node", 11.5,21.5],
     "Fills in the full node name");
-  deepEqual(fillTextValues[1], ["1KiB 100 count", 151.5, 31.5],
+  deepEqual(fillTextValues[1], ["1KiB 100 count", 141.5,21.5],
     "Includes the full byte and count information");
 
   fillTextValues = [];
   node.dx = 250;
-  drawText(ctx, node, borderWidth, ratio, dragZoom);
+  drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
 
-  deepEqual(fillTextValues[0], ["Example Node", 21.5, 31.5],
+  deepEqual(fillTextValues[0], ["Example Node", 11.5,21.5],
     "Fills in the full node name");
   deepEqual(fillTextValues[1], undefined,
     "Drops off the byte and count information if not enough room");
 
   fillTextValues = [];
   node.dx = 100;
-  drawText(ctx, node, borderWidth, ratio, dragZoom);
+  drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
 
-  deepEqual(fillTextValues[0], ["Exampl...", 21.5, 31.5],
+  deepEqual(fillTextValues[0], ["Exampl...", 11.5,21.5],
     "Cuts the name with ellipsis");
   deepEqual(fillTextValues[1], undefined,
     "Drops off the byte and count information if not enough room");
 
   fillTextValues = [];
   node.dx = 40;
-  drawText(ctx, node, borderWidth, ratio, dragZoom);
+  drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
 
-  deepEqual(fillTextValues[0], ["...", 21.5, 31.5],
+  deepEqual(fillTextValues[0], ["...", 11.5,21.5],
     "Shows only ellipsis when smaller");
   deepEqual(fillTextValues[1], undefined,
     "Drops off the byte and count information if not enough room");
 
   fillTextValues = [];
   node.dx = 20;
-  drawText(ctx, node, borderWidth, ratio, dragZoom);
+  drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
 
   deepEqual(fillTextValues[0], undefined,
     "Draw nothing when not enough room");
   deepEqual(fillTextValues[1], undefined,
     "Drops off the byte and count information if not enough room");
 });
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -498,18 +498,16 @@ html, body, #app, #memory-tool {
  * Tree map
  */
 
 .tree-map-container {
   width: 100%;
   height: 100%;
   position: relative;
   overflow: hidden;
-  margin-top: -1em;
-  padding-bottom: 1em;
 }
 
 /**
  * Heap tree errors.
  */
 
 .error::before {
   content: "";