tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

draw.js (9760B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 /**
      6 * Draw the treemap into the provided canvases using the 2d context. The treemap
      7 * layout is computed with d3. There are 2 canvases provided, each matching
      8 * the resolution of the window. The main canvas is a fully drawn version of
      9 * the treemap that is positioned and zoomed using css. It gets blurry the more
     10 * you zoom in as it doesn't get redrawn when zooming. The zoom canvas is
     11 * repositioned absolutely after every change in the dragZoom object, and then
     12 * redrawn to provide a full-resolution (non-blurry) view of zoomed in segment
     13 * of the treemap.
     14 */
     15 
     16 const colorCoarseType = require("resource://devtools/client/memory/components/tree-map/color-coarse-type.js");
     17 const {
     18  hslToStyle,
     19  formatAbbreviatedBytes,
     20  L10N,
     21 } = require("resource://devtools/client/memory/utils.js");
     22 
     23 // A constant fully zoomed out dragZoom object for the main canvas
     24 const NO_SCROLL = {
     25  translateX: 0,
     26  translateY: 0,
     27  zoom: 0,
     28  offsetX: 0,
     29  offsetY: 0,
     30 };
     31 
     32 // Drawing constants
     33 const ELLIPSIS = "...";
     34 const TEXT_MARGIN = 2;
     35 const TEXT_COLOR = "#000000";
     36 const TEXT_LIGHT_COLOR = "rgba(0,0,0,0.5)";
     37 const LINE_WIDTH = 1;
     38 const FONT_SIZE = 10;
     39 const FONT_LINE_HEIGHT = 2;
     40 const PADDING = [5 + FONT_SIZE, 5, 5, 5];
     41 const COUNT_LABEL = L10N.getStr("tree-map.node-count");
     42 
     43 /**
     44 * Setup and start drawing the treemap visualization
     45 *
     46 * @param  {object} report
     47 * @param  {object} canvases
     48 *         A CanvasUtils object that contains references to the main and zoom
     49 *         canvases and contexts
     50 * @param  {object} dragZoom
     51 *         A DragZoom object representing the current state of the dragging
     52 *         and zooming behavior
     53 */
     54 exports.setupDraw = function (report, canvases, dragZoom) {
     55  const getTreemap = configureD3Treemap.bind(null, canvases.main.canvas);
     56 
     57  let treemap, nodes;
     58 
     59  function drawFullTreemap() {
     60    treemap = getTreemap();
     61    nodes = treemap(report);
     62    drawTreemap(canvases.main, nodes, NO_SCROLL);
     63    drawTreemap(canvases.zoom, nodes, dragZoom);
     64  }
     65 
     66  function drawZoomedTreemap() {
     67    drawTreemap(canvases.zoom, nodes, dragZoom);
     68    positionZoomedCanvas(canvases.zoom.canvas, dragZoom);
     69  }
     70 
     71  drawFullTreemap();
     72  canvases.on("resize", drawFullTreemap);
     73  dragZoom.on("change", drawZoomedTreemap);
     74 };
     75 
     76 /**
     77 * Returns a configured d3 treemap function
     78 *
     79 * @param  {HTMLCanvasElement} canvas
     80 * @return {Function}
     81 */
     82 const configureD3Treemap = (exports.configureD3Treemap = function (canvas) {
     83  const window = canvas.ownerDocument.defaultView;
     84  const ratio = window.devicePixelRatio;
     85  const treemap = window.d3.layout
     86    .treemap()
     87    .size([
     88      // The d3 layout includes the padding around everything, add some
     89      // extra padding to the size to compensate for thi
     90      canvas.width + (PADDING[1] + PADDING[3]) * ratio,
     91      canvas.height + (PADDING[0] + PADDING[2]) * ratio,
     92    ])
     93    .sticky(true)
     94    .padding([
     95      PADDING[0] * ratio,
     96      PADDING[1] * ratio,
     97      PADDING[2] * ratio,
     98      PADDING[3] * ratio,
     99    ])
    100    .value(d => d.bytes);
    101 
    102  /**
    103   * Create treemap nodes from a census report that are sorted by depth
    104   *
    105   * @param  {object} report
    106   * @return {Array} An array of d3 treemap nodes
    107   *         // https://github.com/mbostock/d3/wiki/Treemap-Layout
    108   *         parent - the parent node, or null for the root.
    109   *         children - the array of child nodes, or null for leaf nodes.
    110   *         value - the node value, as returned by the value accessor.
    111   *         depth - the depth of the node, starting at 0 for the root.
    112   *         area - the computed pixel area of this node.
    113   *         x - the minimum x-coordinate of the node position.
    114   *         y - the minimum y-coordinate of the node position.
    115   *         z - the orientation of this cell’s subdivision, if any.
    116   *         dx - the x-extent of the node position.
    117   *         dy - the y-extent of the node position.
    118   */
    119  return function depthSortedNodes(report) {
    120    const nodes = treemap(report);
    121    nodes.sort((a, b) => a.depth - b.depth);
    122    return nodes;
    123  };
    124 });
    125 
    126 /**
    127 * Draw the text, cut it in half every time it doesn't fit until it fits or
    128 * it's smaller than the "..." text.
    129 *
    130 * @param  {CanvasRenderingContext2D} ctx
    131 * @param  {number} x
    132 *         the position of the text
    133 * @param  {number} y
    134 *         the position of the text
    135 * @param  {number} innerWidth
    136 *         the inner width of the containing treemap cell
    137 * @param  {Text} name
    138 */
    139 const drawTruncatedName = (exports.drawTruncatedName = function (
    140  ctx,
    141  x,
    142  y,
    143  innerWidth,
    144  name
    145 ) {
    146  const truncated = name.substr(0, Math.floor(name.length / 2));
    147  const formatted = truncated + ELLIPSIS;
    148 
    149  if (ctx.measureText(formatted).width > innerWidth) {
    150    drawTruncatedName(ctx, x, y, innerWidth, truncated);
    151  } else {
    152    ctx.fillText(formatted, x, y);
    153  }
    154 });
    155 
    156 /**
    157 * Fit and draw the text in a node with the following strategies to shrink
    158 * down the text size:
    159 *
    160 * Function 608KB 9083 count
    161 * Function
    162 * Func...
    163 * Fu...
    164 * ...
    165 *
    166 * @param  {CanvasRenderingContext2D} ctx
    167 * @param  {object} node
    168 * @param  {number} borderWidth
    169 * @param  {object} dragZoom
    170 * @param  {Array}  padding
    171 */
    172 const drawText = (exports.drawText = function (
    173  ctx,
    174  node,
    175  borderWidth,
    176  ratio,
    177  dragZoom,
    178  padding
    179 ) {
    180  let { dx, dy, name, totalBytes, totalCount } = node;
    181  const scale = dragZoom.zoom + 1;
    182  dx *= scale;
    183  dy *= scale;
    184 
    185  // Start checking to see how much text we can fit in, optimizing for the
    186  // common case of lots of small leaf nodes
    187  if (FONT_SIZE * FONT_LINE_HEIGHT < dy) {
    188    const margin = borderWidth(node) * 1.5 + ratio * TEXT_MARGIN;
    189    const x = margin + (node.x - padding[0]) * scale - dragZoom.offsetX;
    190    const y = margin + (node.y - padding[1]) * scale - dragZoom.offsetY;
    191    const innerWidth = dx - margin * 2;
    192    const nameSize = ctx.measureText(name).width;
    193 
    194    if (ctx.measureText(ELLIPSIS).width > innerWidth) {
    195      return;
    196    }
    197 
    198    ctx.fillStyle = TEXT_COLOR;
    199 
    200    if (nameSize > innerWidth) {
    201      // The name is too long - halve the name as an expediant way to shorten it
    202      drawTruncatedName(ctx, x, y, innerWidth, name);
    203    } else {
    204      const bytesFormatted = formatAbbreviatedBytes(totalBytes);
    205      const countFormatted = `${totalCount} ${COUNT_LABEL}`;
    206      const byteSize = ctx.measureText(bytesFormatted).width;
    207      const countSize = ctx.measureText(countFormatted).width;
    208      const spaceSize = ctx.measureText(" ").width;
    209 
    210      if (nameSize + byteSize + countSize + spaceSize * 3 > innerWidth) {
    211        // The full name will fit
    212        ctx.fillText(`${name}`, x, y);
    213      } else {
    214        // The full name plus the byte information will fit
    215        ctx.fillText(name, x, y);
    216        ctx.fillStyle = TEXT_LIGHT_COLOR;
    217        ctx.fillText(
    218          `${bytesFormatted} ${countFormatted}`,
    219          x + nameSize + spaceSize,
    220          y
    221        );
    222      }
    223    }
    224  }
    225 });
    226 
    227 /**
    228 * Draw a box given a node
    229 *
    230 * @param  {CanvasRenderingContext2D} ctx
    231 * @param  {object} node
    232 * @param  {number} borderWidth
    233 * @param  {number} ratio
    234 * @param  {object} dragZoom
    235 * @param  {Array}  padding
    236 */
    237 const drawBox = (exports.drawBox = function (
    238  ctx,
    239  node,
    240  borderWidth,
    241  dragZoom,
    242  padding
    243 ) {
    244  const border = borderWidth(node);
    245  const fillHSL = colorCoarseType(node);
    246  const strokeHSL = [fillHSL[0], fillHSL[1], fillHSL[2] * 0.5];
    247  const scale = 1 + dragZoom.zoom;
    248 
    249  // Offset the draw so that box strokes don't overlap
    250  const x = scale * (node.x - padding[0]) - dragZoom.offsetX + border / 2;
    251  const y = scale * (node.y - padding[1]) - dragZoom.offsetY + border / 2;
    252  const dx = scale * node.dx - border;
    253  const dy = scale * node.dy - border;
    254 
    255  ctx.fillStyle = hslToStyle(...fillHSL);
    256  ctx.fillRect(x, y, dx, dy);
    257 
    258  ctx.strokeStyle = hslToStyle(...strokeHSL);
    259  ctx.lineWidth = border;
    260  ctx.strokeRect(x, y, dx, dy);
    261 });
    262 
    263 /**
    264 * Draw the overall treemap
    265 *
    266 * @param  {HTMLCanvasElement} canvas
    267 * @param  {CanvasRenderingContext2D} ctx
    268 * @param  {Array} nodes
    269 * @param  {Objbect} dragZoom
    270 */
    271 const drawTreemap = (exports.drawTreemap = function (
    272  { canvas, ctx },
    273  nodes,
    274  dragZoom
    275 ) {
    276  const window = canvas.ownerDocument.defaultView;
    277  const ratio = window.devicePixelRatio;
    278  const canvasArea = canvas.width * canvas.height;
    279  // Subtract the outer padding from the tree map layout.
    280  const padding = [PADDING[3] * ratio, PADDING[0] * ratio];
    281 
    282  ctx.clearRect(0, 0, canvas.width, canvas.height);
    283  ctx.font = `${FONT_SIZE * ratio}px sans-serif`;
    284  ctx.textBaseline = "top";
    285 
    286  function borderWidth(node) {
    287    const areaRatio = Math.sqrt(node.area / canvasArea);
    288    return ratio * Math.max(1, LINE_WIDTH * areaRatio);
    289  }
    290 
    291  for (let i = 0; i < nodes.length; i++) {
    292    const node = nodes[i];
    293    if (node.parent === undefined) {
    294      continue;
    295    }
    296 
    297    drawBox(ctx, node, borderWidth, dragZoom, padding);
    298    drawText(ctx, node, borderWidth, ratio, dragZoom, padding);
    299  }
    300 });
    301 
    302 /**
    303 * Set the position of the zoomed in canvas. It always take up 100% of the view
    304 * window, but is transformed relative to the zoomed in containing element,
    305 * essentially reversing the transform of the containing element.
    306 *
    307 * @param  {HTMLCanvasElement} canvas
    308 * @param  {object} dragZoom
    309 */
    310 const positionZoomedCanvas = function (canvas, dragZoom) {
    311  const scale = 1 / (1 + dragZoom.zoom);
    312  const x = -dragZoom.translateX;
    313  const y = -dragZoom.translateY;
    314  canvas.style.transform = `scale(${scale}) translate(${x}px, ${y}px)`;
    315 };
    316 
    317 exports.positionZoomedCanvas = positionZoomedCanvas;