block by kforeman 501685b4f7a4d22c1563b519718c4590

Labeled scatter

Full Screen

index.html

<!DOCTYPE html>
<html>
	<head>
	<!-- header/title stuff -->
		<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
		<title>d3 test</title>

	<!-- load in d3 -->
		<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>

	<!-- load in labeler plugin -->
		<script src="labeler.js" charset="utf-8"></script>

	<!-- styling -->
	    <style>
			body {
			  font: 10px sans-serif;
			}
			.axis path,
			.axis line {
			  fill: none;
			  stroke: #000;
			  shape-rendering: crispEdges;
			}
			.dot {
			  stroke: #000;
			  opacity: 0.7;
			}
	    </style>
	</head>
  <body>
    <div id='chart'></div>
    <script type="text/javascript">

    var margin = {top: 20, right: 20, bottom: 40, left: 40},
	    width = 560 - margin.left - margin.right,
	    height = 560 - margin.top - margin.bottom;

	var x = d3.scale.linear()
	    .range([0, width]);

	var y = d3.scale.linear()
	    .range([height, 0]);

	var size = d3.scale.sqrt()
		.range([10,30]);

	var color = d3.scale.category10();

	var xAxis = d3.svg.axis()
	    .scale(x)
	    .tickFormat(d3.format(".0%"))
	    .orient("bottom");

	var yAxis = d3.svg.axis()
	    .scale(y)
	    .tickFormat(d3.format(".0%"))
	    .orient("left");

	var svg = d3.select("body").append("svg")
	    .attr("width", width + margin.left + margin.right)
	    .attr("height", height + margin.top + margin.bottom)
	  .append("g")
	    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    d3.csv('data.csv', function(data) {

	    data.forEach(function(d) {
	    	d.rate1990 = +d.rate1990;
	    	d.rate2015 = +d.rate2015;
	    	d.rate2040 = +d.rate2040;
	    	d.change1 = (Math.log(d.rate2015) - Math.log(d.rate1990)) / (2015-1990);
	    	d.change2 = (Math.log(d.rate2040) - Math.log(d.rate2015)) / (2040-2015);
	    	d.group = d.cause.substr(0,1);
  		});

  		var xe = d3.extent(data, function(d) { return d.change1; }),
  			ye = d3.extent(data, function(d) { return d.change2; }),
  			mn = d3.min([xe[0],ye[0]]),
  			mx = d3.max([xe[1],ye[1]]);

  		mn *= (mn > 0 ? .9 : 1.11);
  		mx *= (mn < 0 ? .9 : 1.11);

  		x.domain([mn,mx]).nice();
  		y.domain([mn,mx]).nice();
  		size.domain(d3.extent(data, function(d) { return d.rate2015; }));

  		svg.append('line')
        	.attr('x1', x.range()[0])
        	.attr('y1', y.range()[0])
        	.attr('x2', x.range()[1])
        	.attr('y2', y.range()[1])
        	.style('stroke', 'black');

  		svg.append("g")
		    .attr("class", "x axis")
		    .attr("transform", "translate(0," + height + ")")
		    .call(xAxis)
	      .append("text")
			.attr("class", "axis-label")
			.attr("x", (margin.left + (width - margin.left - margin.right)/2))
			.attr("y", margin.bottom)
			.style("text-anchor", "middle")
			.text("Rate of change 1990-2015");

		svg.append("g")
	        .attr("class", "y axis")
	        .call(yAxis)
	      .append("text")
			.attr("class", "axis-label")
			.attr("transform", "translate("+(-1*margin.left)+"," +(margin.top + (height - margin.top - margin.bottom)/2) +")rotate(-90)")
			.attr("dy", ".71em")
			.style("text-anchor", "middle")
			.text("Rate of change 2015-2040")

		svg.selectAll(".dot")
			.data(data)
		  .enter().append("circle")
			.attr("class", "dot")
			.attr("r", function(d) { return size(d.rate2015); })
			.attr("cx", function(d) { return x(d.change1); })
			.attr("cy", function(d) { return y(d.change2); })
			.style("fill", function(d) { return color(d.group); });

		var legend = svg.selectAll(".legend")
			.data(color.domain())
	  	  .enter().append("g")
			.attr("class", "legend")
			.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

		legend.append("rect")
			.attr("x", 6)
			.attr("width", 18)
			.attr("height", 18)
			.style("fill", color);

		legend.append("text")
			.attr("x", 32)
			.attr("y", 9)
			.attr("dy", ".35em")
			.text(function(d) { return d; });

		var label_array = data.map(function(d) { return {
			'x': x(d.change1),
			'y': y(d.change2),
			'name': d.cause
		}; });
		var anchor_array = data.map(function(d) { return {
			'x': x(d.change1),
			'y': y(d.change2),
			'r': size(d.rate2015)*1.05 
		}; });

        // Draw labels
        labels = svg.selectAll(".label")
            .data(label_array)
            .enter()
            .append("text")
            .attr("class", "label")
            .attr('text-anchor', 'start')
            .text(function(d) { return d.name; })
            .attr("x", function(d) { return (d.x); })
            .attr("y", function(d) { return (d.y); })
            .attr("fill", "black");

        var index = 0;
        labels.each(function() {
            label_array[index].width = this.getBBox().width;
            label_array[index].height = this.getBBox().height;
            index += 1;
        });

        // Setup labels
        var sim_ann = d3.labeler()
            .label(label_array)
            .anchor(anchor_array)
            .width(width)
            .height(height)
            .start(1000);

        labels
	        .transition()
	        .duration(300)
	        .attr("x", function(d) { return (d.x); })
	        .attr("y", function(d) { return (d.y); });

    });

    </script>
  </body>
</html>

data.csv

cause,rate1990,rate2015,rate2040
A1,5,3,0.5
A2,5,2,4
A3,5,4,0.5
B1,5,7,10
B2,5,8,3
C1,5,10,10
C2,5,6,15

labeler.js

(function() {

d3.labeler = function() {
  var lab = [],
      anc = [],
      w = 1, // box width
      h = 1, // box width
      labeler = {};

  var max_move = 5.0,
      max_angle = 0.5,
      acc = 0;
      rej = 0;

  // weights
  var w_len = 0.2, // leader line length 
      w_inter = 1.0, // leader line intersection
      w_lab2 = 30.0, // label-label overlap
      w_lab_anc = 30.0; // label-anchor overlap
      w_orient = 3.0; // orientation bias

  // booleans for user defined functions
  var user_energy = false,
      user_schedule = false;

  var user_defined_energy, 
      user_defined_schedule;

  energy = function(index) {
  // energy function, tailored for label placement

      var m = lab.length, 
          ener = 0,
          dx = lab[index].x - anc[index].x,
          dy = anc[index].y - lab[index].y,
          dist = Math.sqrt(dx * dx + dy * dy),
          overlap = true,
          amount = 0
          theta = 0;

      // penalty for length of leader line
      if (dist > 0) ener += dist * w_len;

      // label orientation bias
      dx /= dist;
      dy /= dist;
      if (dx > 0 && dy > 0) { ener += 0 * w_orient; }
      else if (dx < 0 && dy > 0) { ener += 1 * w_orient; }
      else if (dx < 0 && dy < 0) { ener += 2 * w_orient; }
      else { ener += 3 * w_orient; }

      var x21 = lab[index].x,
          y21 = lab[index].y - lab[index].height + 2.0,
          x22 = lab[index].x + lab[index].width,
          y22 = lab[index].y + 2.0;
      var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area;

      for (var i = 0; i < m; i++) {
        if (i != index) {

          // penalty for intersection of leader lines
          overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x,
                          anc[index].y, lab[index].y, anc[i].y, lab[i].y);
          if (overlap) ener += w_inter;

          // penalty for label-label overlap
          x11 = lab[i].x;
          y11 = lab[i].y - lab[i].height + 2.0;
          x12 = lab[i].x + lab[i].width;
          y12 = lab[i].y + 2.0;
          x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
          y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
          overlap_area = x_overlap * y_overlap;
          ener += (overlap_area * w_lab2);
          }

          // penalty for label-anchor overlap
          x11 = anc[i].x - anc[i].r;
          y11 = anc[i].y - anc[i].r;
          x12 = anc[i].x + anc[i].r;
          y12 = anc[i].y + anc[i].r;
          x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
          y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
          overlap_area = x_overlap * y_overlap;
          ener += (overlap_area * w_lab_anc);

      }
      return ener;
  };

  mcmove = function(currT) {
  // Monte Carlo translation move

      // select a random label
      var i = Math.floor(Math.random() * lab.length); 

      // save old coordinates
      var x_old = lab[i].x;
      var y_old = lab[i].y;

      // old energy
      var old_energy;
      if (user_energy) {old_energy = user_defined_energy(i, lab, anc)}
      else {old_energy = energy(i)}

      // random translation
      lab[i].x += (Math.random() - 0.5) * max_move;
      lab[i].y += (Math.random() - 0.5) * max_move;

      // hard wall boundaries
      if (lab[i].x > w) lab[i].x = x_old;
      if (lab[i].x < 0) lab[i].x = x_old;
      if (lab[i].y > h) lab[i].y = y_old;
      if (lab[i].y < 0) lab[i].y = y_old;

      // new energy
      var new_energy;
      if (user_energy) {new_energy = user_defined_energy(i, lab, anc)}
      else {new_energy = energy(i)}

      // delta E
      var delta_energy = new_energy - old_energy;

      if (Math.random() < Math.exp(-delta_energy / currT)) {
        acc += 1;
      } else {
        // move back to old coordinates
        lab[i].x = x_old;
        lab[i].y = y_old;
        rej += 1;
      }

  };

  mcrotate = function(currT) {
  // Monte Carlo rotation move

      // select a random label
      var i = Math.floor(Math.random() * lab.length); 

      // save old coordinates
      var x_old = lab[i].x;
      var y_old = lab[i].y;

      // old energy
      var old_energy;
      if (user_energy) {old_energy = user_defined_energy(i, lab, anc)}
      else {old_energy = energy(i)}

      // random angle
      var angle = (Math.random() - 0.5) * max_angle;

      var s = Math.sin(angle);
      var c = Math.cos(angle);

      // translate label (relative to anchor at origin):
      lab[i].x -= anc[i].x
      lab[i].y -= anc[i].y

      // rotate label
      var x_new = lab[i].x * c - lab[i].y * s,
          y_new = lab[i].x * s + lab[i].y * c;

      // translate label back
      lab[i].x = x_new + anc[i].x
      lab[i].y = y_new + anc[i].y

      // hard wall boundaries
      if (lab[i].x > w) lab[i].x = x_old;
      if (lab[i].x < 0) lab[i].x = x_old;
      if (lab[i].y > h) lab[i].y = y_old;
      if (lab[i].y < 0) lab[i].y = y_old;

      // new energy
      var new_energy;
      if (user_energy) {new_energy = user_defined_energy(i, lab, anc)}
      else {new_energy = energy(i)}

      // delta E
      var delta_energy = new_energy - old_energy;

      if (Math.random() < Math.exp(-delta_energy / currT)) {
        acc += 1;
      } else {
        // move back to old coordinates
        lab[i].x = x_old;
        lab[i].y = y_old;
        rej += 1;
      }
      
  };

  intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) {
  // returns true if two lines intersect, else false
  // from http://paulbourke.net/geometry/lineline2d/

    var mua, mub;
    var denom, numera, numerb;

    denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
    numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);

    /* Is the intersection along the the segments */
    mua = numera / denom;
    mub = numerb / denom;
    if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) {
        return true;
    }
    return false;
  }

  cooling_schedule = function(currT, initialT, nsweeps) {
  // linear cooling
    return (currT - (initialT / nsweeps));
  }

  labeler.start = function(nsweeps) {
  // main simulated annealing function
      var m = lab.length,
          currT = 1.0,
          initialT = 1.0;

      for (var i = 0; i < nsweeps; i++) {
        for (var j = 0; j < m; j++) { 
          if (Math.random() < 0.5) { mcmove(currT); }
          else { mcrotate(currT); }
        }
        currT = cooling_schedule(currT, initialT, nsweeps);
      }
  };

  labeler.width = function(x) {
  // users insert graph width
    if (!arguments.length) return w;
    w = x;
    return labeler;
  };

  labeler.height = function(x) {
  // users insert graph height
    if (!arguments.length) return h;
    h = x;    
    return labeler;
  };

  labeler.label = function(x) {
  // users insert label positions
    if (!arguments.length) return lab;
    lab = x;
    return labeler;
  };

  labeler.anchor = function(x) {
  // users insert anchor positions
    if (!arguments.length) return anc;
    anc = x;
    return labeler;
  };

  labeler.alt_energy = function(x) {
  // user defined energy
    if (!arguments.length) return energy;
    user_defined_energy = x;
    user_energy = true;
    return labeler;
  };

  labeler.alt_schedule = function(x) {
  // user defined cooling_schedule
    if (!arguments.length) return  cooling_schedule;
    user_defined_schedule = x;
    user_schedule = true;
    return labeler;
  };

  return labeler;
};

})();