block by nitaku 79f16392f7e1bed77f07

Word cloud treemap (flare)

Full Screen

TBC This experiment tries to solve some problems of word clouds by using a treemap layout to create one. See the chapter about Wordle in Steele and Iliinski’s Beautiful Visualization (page 37).

index.js

(function() {
  var SCALE, color, correct_x, correct_y, height, svg, treemap, vis, width, zoom, zoomable_layer;

  SCALE = 400;

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

  correct_x = d3.scale.linear().domain([0, SCALE]).range([0, 420]);

  correct_y = d3.scale.linear().domain([0, SCALE]).range([0, 300]);

  color = function(txt, light) {
    var noise;

    Math.seedrandom(txt + 'abcdef');
    noise = function(W) {
      return Math.random() * W - W / 2;
    };
    return d3.hcl(0 + noise(360), 20, light ? 65 : 25);
  };

  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([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 enter_labels, labels, nodes_data;

    nodes_data = treemap.nodes(tree);
    labels = vis.selectAll('.label').data(nodes_data.filter(function(node) {
      return node.depth === 1;
    }));
    enter_labels = labels.enter().append('svg').attr({
      "class": 'label'
    });
    enter_labels.append('text').text(function(node) {
      return node.name.toUpperCase();
    }).attr({
      dy: '0.35em',
      fill: function(node) {
        return color(node.name, false);
      }
    }).each(function(node) {
      var bbox, bbox_aspect, node_bbox, node_bbox_aspect, rotate;

      bbox = this.getBBox();
      bbox_aspect = bbox.width / bbox.height;
      node_bbox = {
        width: node.dx,
        height: node.dy
      };
      node_bbox_aspect = node_bbox.width / node_bbox.height;
      rotate = bbox_aspect >= 1 && node_bbox_aspect < 1 || bbox_aspect < 1 && node_bbox_aspect >= 1;
      node.label_bbox = {
        x: bbox.x + (bbox.width - correct_x(bbox.width)) / 2,
        y: bbox.y + (bbox.height - correct_y(bbox.height)) / 2,
        width: correct_x(bbox.width),
        height: correct_y(bbox.height)
      };
      if (rotate) {
        node.label_bbox = {
          x: node.label_bbox.y,
          y: node.label_bbox.x,
          width: node.label_bbox.height,
          height: node.label_bbox.width
        };
        return d3.select(this).attr('transform', 'rotate(-90)');
      }
    });
    return enter_labels.attr({
      x: function(node) {
        return node.x;
      },
      y: function(node) {
        return node.y;
      },
      width: function(node) {
        return node.dx;
      },
      height: function(node) {
        return node.dy;
      },
      viewBox: function(node) {
        return "" + node.label_bbox.x + " " + node.label_bbox.y + " " + node.label_bbox.width + " " + node.label_bbox.height;
      },
      preserveAspectRatio: 'none'
    });
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
	<head>
        <meta charset="utf-8">
        <meta name="description" content="Word cloud treemap (flare)" />
        <title>Word cloud treemap (flare)</title>
		<link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//davidbau.com/encode/seedrandom-min.js"></script>
        <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

# layout, behaviors and scales
SCALE = 400

treemap = d3.layout.treemap()
  .size([SCALE, SCALE])
  .value((node) -> node.size)
  
correct_x = d3.scale.linear()
  .domain([0, SCALE])
  .range([0, 420])
correct_y = d3.scale.linear()
  .domain([0, SCALE])
  .range([0, 300])
  
color = (txt, light) ->
  Math.seedrandom(txt+'abcdef')
  noise = (W) -> Math.random()*W - W/2
  d3.hcl(0+noise(360), 20, if light then 65 else 25)
  
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([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) ->
  nodes_data = treemap.nodes(tree)
  
  #nodes = vis.selectAll('.node')
  #  .data(nodes_data.filter((node) -> node.depth is 1))
  #  
  #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: (node) -> color(node.name, true)
  
  labels = vis.selectAll('.label')
      .data(nodes_data.filter((node) -> node.depth is 1))
  
  enter_labels = labels.enter().append('svg')
      .attr
        class: 'label'
    
  enter_labels.append('text')
      .text((node) -> node.name.toUpperCase())
      .attr
        dy: '0.35em'
        fill: (node) -> color(node.name, false)
      .each (node) ->
        bbox = this.getBBox()
        bbox_aspect = bbox.width / bbox.height
        
        node_bbox = {width: node.dx, height: node.dy}
        node_bbox_aspect = node_bbox.width / node_bbox.height
        
        rotate = bbox_aspect >= 1 and node_bbox_aspect < 1 or bbox_aspect < 1 and node_bbox_aspect >= 1
        node.label_bbox = {
          x: bbox.x+(bbox.width-correct_x(bbox.width))/2,
          y: bbox.y+(bbox.height-correct_y(bbox.height))/2,
          width: correct_x(bbox.width),
          height: correct_y(bbox.height)
        }
        
        if rotate
          node.label_bbox = {
            x: node.label_bbox.y,
            y: node.label_bbox.x,
            width: node.label_bbox.height,
            height: node.label_bbox.width
          }
          d3.select(this).attr('transform', 'rotate(-90)')
        
  
  enter_labels
    .attr
      x: (node) -> node.x
      y: (node) -> node.y
      width: (node) -> node.dx
      height: (node) -> node.dy
      viewBox: (node) -> "#{node.label_bbox.x} #{node.label_bbox.y} #{node.label_bbox.width} #{node.label_bbox.height}"
      preserveAspectRatio: 'none'
      

index.css

svg {
  background: white;
}
.node {
  shape-rendering: crispEdges;
  vector-effect: non-scaling-stroke;
  stroke: white;
  stroke-width: 2;
}
.label {
  pointer-events: none;
  text-anchor: middle;
  font-family: Impact;
}