block by mpmckenna8 429a8eb651ce35a252da

Automatic floating labels using d3 force-layout

Full Screen

index.html

<!DOCTYPE html>
<html>
<head>
    <script src="//d3js.org/d3.v2.js"></script>
    <script type="text/javascript" src="force_labels.js"></script>
    <style>
    .anchor { fill:blue}
    .labelbox { fill:black;opacity:0.8}
    .labeltext { fill:white;font-weight:bold;text-anchor:middle;font-size:16;font-family:  serif}
    .link { stroke:gray;stroke-width:0.35}
    </style>
</head>
		
<body>
    <div style="width:150px;float:left">
        <span id="corr-label">Correlation: </span><br>
        <input type="range" min="-1.0" max="1.0" value="0.0" id="corr" step="0.01"/>
    </div>
    <div style="width:150px;float:left">
        <span id="charge-label">Label charge: </span><br>
        <input type="range" min="0" max="100" value="60.0" id="charge" step="1"/>
    </div>
    <button type="button" id="addone">Add one measurement</button>
    <button type="button" id="randomize20">Replace with 20</button>
    <button type="button" id="randomize50">Replace with 50</button>
    <button type="button" id="randomize100">Replace with 100</button>

<script type="text/javascript">
var w=960,h=500,
    x_mean = w/2,
    x_std = w/10,
    y_mean = h/2,
    y_std = h/10,
    labelBox,link,
    data=[];


var svg=d3.select("body")
    .append("svg:svg")
    .attr("height",h)
    .attr("width",w)
    
function refresh() {
    // plot the data as usual
    anchors = svg.selectAll(".anchor").data(data,function(d,i) { return i})
    anchors.exit().attr("class","exit").transition().duration(1000).style("opacity",0).remove()
    anchors.enter().append("circle").attr("class","anchor").attr("r",4).attr("cx",function(d) { return d.x}).attr("cy",function(d) { return h-d.y})
    anchors.transition()
        .delay(function(d,i) { return i*10})
        .duration(1500)
        .attr("cx",function(d) { return d.x})
        .attr("cy",function(d) { return h-d.y})
    
        
    // Now for the labels
    anchors.call(labelForce.update)  //  This is the only function call needed, the rest is just drawing the labels
       
    labels = svg.selectAll(".labels").data(data,function(d,i) { return i})
    labels.exit().attr("class","exit").transition().delay(0).duration(500).style("opacity",0).remove()
    
    // Draw the labelbox, caption and the link
        newLabels = labels.enter().append("g").attr("class","labels")

        newLabelBox = newLabels.append("g").attr("class","labelbox")
                newLabelBox.append("circle").attr("r",11)
                newLabelBox.append("text").attr("class","labeltext").attr("y",6)
        newLabels.append("line").attr("class","link")
        
        labelBox = svg.selectAll(".labels").selectAll(".labelbox")
        links = svg.selectAll(".link")
        labelBox.selectAll("text").text(function(d) { return d.num})

}

function redrawLabels() {
    labelBox
        .attr("transform",function(d) { return "translate("+d.labelPos.x+" "+d.labelPos.y+")"})

    links
        .attr("x1",function(d) { return d.anchorPos.x})
        .attr("y1",function(d) { return d.anchorPos.y})
        .attr("x2",function(d) { return d.labelPos.x})
        .attr("y2",function(d) { return d.labelPos.y})
}        
        

// Initialize the label-forces
labelForce = d3.force_labels()
    .linkDistance(0.0)
    .gravity(0)
    .nodes([]).links([])
    .charge(-60)
    .on("tick",redrawLabels)



// and now for the data functionality
function randomize(count) {
    z1=d3.random.normal()
    z2=d3.random.normal()
    data=data.concat(d3.range(count || 100).map(function(d,i) { return {z1:z1(),z2:z2(),num:data.length+i}}))
    correlate()
}

function correlate() {
    var corr = d3.select("#corr").property("value")
    d3.select("#corr-label").text("Correlation: "+d3.format("%")(corr))
    
    data.forEach(function(d) { d.x = x_mean+(d.z1*x_std),
                                d.y = y_mean+y_std*(corr*d.z1+d.z2*Math.sqrt(1-Math.pow(corr,2)))})
    refresh()
}

// and finally hook up the controls

d3.select("#randomize20").on("click",function() { data=[];randomize(20)})
d3.select("#randomize50").on("click",function() { data=[];randomize(50)})
d3.select("#randomize100").on("click",function() { data=[];randomize(100)})
d3.select("#addone").on("click",function() { randomize(1)})
d3.select("#corr")
    .on("change",function() { d3.select("#corr-label").text("Correlation: "+d3.format("%")(this.value))})
    .on("mouseup",correlate)
d3.select("#charge")
    .on("change",function() { 
        d3.select("#charge-label").text("Label charge: "+d3.format("f")(this.value))
        labelForce.charge(-this.value).start()
    })

randomize()        
</script>
</body>
</html>

force_labels.js

(function() {
  d3.force_labels = function force_labels() {    
    var labels = d3.layout.force();
      
    // Update the position of the anchor based on the center of bounding box
    function updateAnchor() {
      if (!labels.selection) return;
      labels.selection.each(function(d) {
        var bbox = this.getBBox(),
            x = bbox.x + bbox.width / 2,
            y = bbox.y + bbox.height / 2;

        d.anchorPos.x = x;
        d.anchorPos.y = y;
       
        // If a label position does not exist, set it to be the anchor position 
        if (d.labelPos.x == null) {
          d.labelPos.x = x;
          d.labelPos.y = y;
        }
      });
    }
    
    //The anchor position should be updated on each tick
    labels.on("tick.labels", updateAnchor);
    
    // This updates all nodes/links - retaining any previous labelPos on updated nodes
    labels.update = function(selection) {
      labels.selection = selection;
      var nodes = [], links = [];
      selection[0].forEach(function(d) {    
        if(d && d.__data__) {
          var data = d.__data__;
          
          if (!d.labelPos) d.labelPos = {fixed: false};
          if (!d.anchorPos) d.anchorPos = {fixed: true};
          
          // Place position objects in __data__ to make them available through 
          // d.labelPos/d.anchorPos for different elements
          data.labelPos = d.labelPos;
          data.anchorPos = d.anchorPos;
          
          links.push({target: d.anchorPos, source: d.labelPos});
          nodes.push(d.anchorPos);
          nodes.push(d.labelPos);
        }
      });
      labels
          .stop()
          .nodes(nodes)
          .links(links);
      updateAnchor();
      labels.start();
    };
    return labels;
  };
})();