block by nitaku 564b91e8e9e7cbc7821f

Treemap navigator (flare)

Full Screen

A combination of a treemap with a node-link diagram, showing the flare software package hierarchy. Click on a treemap region to create a new node displaying the corresponding subtree. Subtrees can be dragged around, and the whole visualization can be navigated with zoom and pan.

This experiment tries to address a known issue of treemaps: while they are very compact and good for presenting a tree’s overview, they fail to unambiguosly and clearly represent hierarchical structure. Node-link diagrams, on the other hand, are very good at this, but fail to scale to big trees and do not provide a nice overview.

The presented technique, while developed independently, is similar to Elastic Hierarchies, described in Zhao et al. 2005.

index.js

(function() {
  var PADDING, SCALE, drag, height, redraw, subtrees_data, svg, treemap, vis, width, zoom, zoomable_layer;

  subtrees_data = [];

  SCALE = 200;

  PADDING = 8;

  treemap = d3.layout.treemap().size([SCALE, SCALE]).value(function(node) {
    return node.size;
  });

  drag = d3.behavior.drag().origin(function(d) {
    return d;
  });

  drag.on('dragstart', function() {
    return d3.event.sourceEvent.stopPropagation();
  });

  drag.on('drag', function(d) {
    d.x = d3.event.x;
    d.y = d3.event.y;
    return redraw();
  });

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  svg.attr({
    viewBox: "" + (-width / 2) + " " + (-height / 2) + " " + width + " " + height
  });

  zoomable_layer = svg.append('g');

  zoom = d3.behavior.zoom().scaleExtent([0.1, 10]).on('zoom', function() {
    return zoomable_layer.attr({
      transform: "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")"
    });
  });

  svg.call(zoom);

  vis = zoomable_layer.append('g').attr({
    transform: "translate(" + (-SCALE / 2) + "," + (-SCALE / 2) + ")"
  });

  d3.json('http://wafi.iit.cnr.it/webvis/tmp/flare.json', function(tree) {
    var subtree_data;

    treemap.nodes(tree);
    subtree_data = {
      tree: tree,
      x: 0,
      y: 0
    };
    tree.subtree = subtree_data;
    subtrees_data.push(subtree_data);
    return redraw();
  });

  redraw = function(duration) {
    var enter_nodes, enter_subtrees, links, nodes, subtrees;

    duration = duration != null ? duration : 0;
    links = vis.selectAll('.link').data(subtrees_data.filter(function(subtree_data) {
      return subtree_data.tree.depth !== 0;
    }));
    links.enter().append('path').attr({
      "class": 'link'
    });
    links.transition().duration(duration).attr({
      d: function(subtree_data) {
        var parent;

        parent = subtree_data.tree.parent.subtree;
        return "M" + (parent.x + subtree_data.tree.x + subtree_data.tree.dx / 2) + " " + (parent.y + subtree_data.tree.y + subtree_data.tree.dy / 2) + " L" + (subtree_data.x + subtree_data.tree.x + subtree_data.tree.dx / 2) + " " + (subtree_data.y + subtree_data.tree.y + subtree_data.tree.dy / 2);
      }
    });
    subtrees = vis.selectAll('.subtree').data(subtrees_data);
    enter_subtrees = subtrees.enter().append('g').call(drag).attr({
      "class": 'subtree'
    });
    enter_subtrees.append('rect').attr({
      "class": 'handle',
      x: function(subtree_data) {
        return subtree_data.tree.x - PADDING;
      },
      y: function(subtree_data) {
        return subtree_data.tree.y - PADDING;
      },
      width: function(subtree_data) {
        return subtree_data.tree.dx + 2 * PADDING;
      },
      height: function(subtree_data) {
        return subtree_data.tree.dy + 2 * PADDING;
      }
    });
    enter_subtrees.append('text').text(function(subtree_data) {
      return subtree_data.tree.name;
    }).attr({
      "class": 'label',
      x: function(subtree_data) {
        return subtree_data.tree.x + subtree_data.tree.dx / 2;
      },
      y: function(subtree_data) {
        return subtree_data.tree.y;
      },
      dy: '-1em'
    });
    subtrees.transition().duration(duration).attr({
      transform: function(subtree_data) {
        return "translate(" + subtree_data.x + "," + subtree_data.y + ")";
      }
    });
    nodes = subtrees.selectAll('.node').data(function(subtree_data) {
      if (subtree_data.tree.children != null) {
        return subtree_data.tree.children;
      } else {
        return [];
      }
    });
    enter_nodes = nodes.enter().append('rect').attr({
      "class": 'node',
      x: function(node) {
        return node.x;
      },
      y: function(node) {
        return node.y;
      },
      width: function(node) {
        return node.dx;
      },
      height: function(node) {
        return node.dy;
      },
      fill: 'transparent'
    });
    enter_nodes.on('click', function(node) {
      var subtree_data, x, y;

      if (node.exploded === true) {
        return;
      }
      node.exploded = true;
      x = d3.select(this.parentNode).datum().x;
      y = d3.select(this.parentNode).datum().y;
      subtree_data = {
        tree: node,
        x: x,
        y: y
      };
      node.subtree = subtree_data;
      subtrees_data.push(subtree_data);
      redraw();
      subtree_data.x += 30;
      subtree_data.y += 30;
      return window.requestAnimationFrame(function() {
        return redraw(1000);
      });
    });
    enter_nodes.append('title').text(function(node) {
      return node.name;
    });
    return nodes.classed('exploded', function(node) {
      return node.exploded;
    });
  };

}).call(this);

index.html

<!DOCTYPE html>
<html>
	<head>
        <meta charset="utf-8">
        <meta name="description" content="Treemap navigator (flare)" />
        <title>Treemap navigator (flare)</title>
		<link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
	</head>
	<body>
        <svg height="500" width="960"></svg>
        <script src="index.js"></script>
	</body>
</html>

index.coffee

subtrees_data = []

# layout, behaviors and scales
SCALE = 200
PADDING = 8

treemap = d3.layout.treemap()
  .size([SCALE, SCALE])
  .value((node) -> node.size)
  
# define a drag behavior
drag = d3.behavior.drag()
  .origin((d) -> d)
  
drag.on 'dragstart', () ->
  # silence other listeners (disbale pan when dragging)
  # see https://github.com/mbostock/d3/wiki/Drag-Behavior
  d3.event.sourceEvent.stopPropagation()
  
drag.on 'drag', (d) ->
  # update the datum of the dragged node
  d.x = d3.event.x
  d.y = d3.event.y
  
  redraw()
  
svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

# translate the viewBox to have (0,0) at the center of the vis
svg
  .attr
    viewBox: "#{-width/2} #{-height/2} #{width} #{height}"
    
# append a group for zoomable content
zoomable_layer = svg.append('g')
 
# define a zoom behavior
zoom = d3.behavior.zoom()
  .scaleExtent([0.1,10]) # min-max zoom
  .on 'zoom', () ->
    # GEOMETRIC ZOOM
    zoomable_layer
      .attr
        transform: "translate(#{zoom.translate()})scale(#{zoom.scale()})"

# bind the zoom behavior to the main SVG
svg.call(zoom)

# group the visualization
vis = zoomable_layer.append('g')
  .attr
    transform: "translate(#{-SCALE/2},#{-SCALE/2})"

    
d3.json 'http://wafi.iit.cnr.it/webvis/tmp/flare.json', (tree) ->
  treemap.nodes(tree)
  subtree_data = {tree: tree, x: 0, y: 0}
  tree.subtree = subtree_data
  subtrees_data.push subtree_data
  redraw()
  
redraw = (duration) ->
  duration = if duration? then duration else 0
  
  # links
  links = vis.selectAll('.link')
    .data(subtrees_data.filter((subtree_data) -> subtree_data.tree.depth isnt 0))
    
  links.enter().append('path')
    .attr
      class: 'link'
  
  links.transition().duration(duration)
    .attr
      d: (subtree_data) ->
        parent = subtree_data.tree.parent.subtree
        return "M#{parent.x+subtree_data.tree.x+subtree_data.tree.dx/2} #{parent.y+subtree_data.tree.y+subtree_data.tree.dy/2} L#{subtree_data.x+subtree_data.tree.x+subtree_data.tree.dx/2} #{subtree_data.y+subtree_data.tree.y+subtree_data.tree.dy/2}"
  
  # subtrees
  subtrees = vis.selectAll('.subtree')
    .data(subtrees_data)
    
  enter_subtrees = subtrees.enter().append('g')
    .call(drag)
    .attr
      class: 'subtree'
        
  enter_subtrees.append('rect')
    .attr
      class: 'handle'
      x: (subtree_data) -> subtree_data.tree.x - PADDING
      y: (subtree_data) -> subtree_data.tree.y - PADDING
      width: (subtree_data) -> subtree_data.tree.dx + 2*PADDING
      height: (subtree_data) -> subtree_data.tree.dy + 2*PADDING
      
  enter_subtrees.append('text')
    .text((subtree_data) -> subtree_data.tree.name)
    .attr
      class: 'label'
      x: (subtree_data) -> subtree_data.tree.x + subtree_data.tree.dx/2
      y: (subtree_data) -> subtree_data.tree.y
      dy: '-1em'
      
  subtrees.transition().duration(duration)
    .attr
      transform: (subtree_data) -> "translate(#{subtree_data.x},#{subtree_data.y})"
  
  
  nodes = subtrees.selectAll('.node')
    .data((subtree_data) -> if subtree_data.tree.children? then subtree_data.tree.children else [])
    
  enter_nodes = nodes.enter().append('rect')
    .attr
      class: 'node'
      x: (node) -> node.x
      y: (node) -> node.y
      width: (node) -> node.dx
      height: (node) -> node.dy
      fill: 'transparent'
  
  enter_nodes.on 'click', (node) ->
      return if node.exploded is true
      node.exploded = true
      
      x = d3.select(this.parentNode).datum().x
      y = d3.select(this.parentNode).datum().y
      subtree_data = {tree: node, x: x, y: y}
      node.subtree = subtree_data
      subtrees_data.push subtree_data
      redraw()
      
      # push new subtrees away from the point of creation
      subtree_data.x += 30
      subtree_data.y += 30
      window.requestAnimationFrame () -> redraw(1000)
      
      
  enter_nodes.append('title')
    .text((node) -> node.name)
      
  nodes.classed('exploded', (node) -> node.exploded)
  

index.css

svg {
  background: white;
}
.node {
  stroke-width: 1px;
  shape-rendering: crispEdges;
  vector-effect: non-scaling-stroke;
  stroke: #BBB;
  fill: white;
}
.handle {
  stroke-width: 1px;
  shape-rendering: crispEdges;
  vector-effect: non-scaling-stroke;
  fill: white;
  stroke: #333;
}
.label {
  text-anchor: middle;
  font-family: sans-serif;
  /*text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;*/
  text-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white, -1px -1px white, 1px -1px white, 1px 1px white, -1px 1px white;
}
.link {
  fill: none;
  stroke: gray;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}

.node:hover {
  fill: #EEE;
  cursor: pointer;
}
.node.exploded {
  fill: #EEE;
}
.handle:hover {
  stroke-width: 2px;
  cursor: move;
}