block by patricksurry 7089206

Force layout with dynamic distance function

Full Screen

Click and drag to move states around. Move slider to modify distance function

Fork of mbostock’s force layout example (http://bl.ocks.org/mbostock/1073373) with a few experimental changes.

Create links between neighbouring states (inferred from topojson mesh) rather than from Voronoi triangles which add some unstable cross-country links like Maine-Washington. Parameterize distance function with a slider (0 = spatial similarity only, 1 = more variation based on type similarity).

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Force-Directed States of America</title>
        <script src=""></script>

    <script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script>
    <script type="text/javascript" src="//d3js.org/topojson.v1.min.js"></script>
    <script src="//d3js.org/queue.v1.min.js"></script>
    <script src="d3.slider.js"></script>
<!--    <script type="text/javascript" src="//d3js.org/d3.geom.js"></script>
    <script type="text/javascript" src="//d3js.org/d3.layout.js"></script>
-->
  <link rel="stylesheet" href="d3.slider.css" />  

    <style type="text/css">

path {
  fill: #ddd;
  fill-opacity: .8;
  stroke: #fff;
  stroke-width: 1.5px;
}

line {
  stroke: #999;
}

#slider {
    width: 80%;
    margin: auto;
}

    </style>
  </head>
  <body>
    <div id='slidervalue'></div>
    <script type="text/javascript">

var w = 1024,
    h = 600;

var projection = d3.geo.albersUsa().translate([w/2,h/2]),
    path = d3.geo.path().projection(projection),
    force = d3.layout.force().size([w, h]);

var svg = d3.select("body").append("svg:svg")
    .attr("width", w)
    .attr("height", h);

var slider = d3.slider()
    .axis(true)
    .min(0)
    .max(1)
    .step(0.01)
    .on('slide', function(e,v) {
        force.start();
    });
    
d3.select("body")
    .append("div")
    .attr("id","slider")
    .call(slider);

queue()
    .defer(d3.json, "world-50m.json")
    .defer(d3.csv, "similarity.csv")
    .await(ready);

function linkDistance(d) {
    var ds = (1. / d.ss - 1)*23,
        tf = Math.exp(slider.value()*(1.-8*d.ts));      
    return ds * tf;  //d.distance; 
   }    
   
function ready(error, world, similarity) {
  var nodes = [],
      links = [],
      nodesbyname = {};
      
    function continentalus(o) {
        return o.properties.iso_a2 == 'US' 
                && !( o.properties.postal == 'HI' || o.properties.postal == 'AK'); 
    }
    
  var states = topojson.feature(world, world.objects.states).features
        .filter(continentalus);
  states.forEach(function(d, i) {
    //if (d.id == "02" || d.id == "15" || d.id == "72" || d.id == "78") return; // excl AK,HI,PR
    var centroid = path.centroid(d);
    centroid.x = centroid[0];
    centroid.y = centroid[1];
    centroid.feature = d;
    if (d.properties.name == 'Kansas') centroid.fixed = true;
    nodes.push(centroid);
    nodesbyname[d.properties.name] = centroid;
    /*
    for (var j=0; j<i; j++) {
        links.push(edge(centroid,nodes[j]));
    }
    */
  });

    var neighbours = { 'New York_Michigan' : true, 'Michigan_New York' : true };

/*
  d3.geom.voronoi()
    .links(nodes)
    .forEach(function(lnk) {
        var a = lnk.source.feature.properties.name, 
            b = lnk.target.feature.properties.name;        
        neighbours[a + '_' + b] = true;
        neighbours[b + '_' + a] = true;
      });
*/
    
    topojson.mesh(world, world.objects.states, function (a, b) {
        var aok = continentalus(a),
            bok = continentalus(b);
        if (aok && bok && a !== b) {
            neighbours[a.properties.name + '_' + b.properties.name] = true;
            neighbours[b.properties.name + '_' + a.properties.name] = true;
        }
         
        return false;
    });
    
  similarity.forEach(function(r){
    if (!(r.a in nodesbyname && r.b in nodesbyname)) return;
    if (!(r.a + '_' + r.b in neighbours)) return;
    var e = {
        source: nodesbyname[r.a],
        target: nodesbyname[r.b],
        ss: +r.spatialsimilarity,
        ts: +r.typesimilarity
        };
    links.push(e);
  });

    // randomize position
/*    nodes.forEach(function(d) {

        var abbrev = d.feature.properties.postal;
        if (abbrev == 'FL' || abbrev == 'WA' || abbrev == 'ME') {
            d.fixed = true;
            return;
        }
        
        d.x = w *(0.5 + 0.5* (Math.random()-0.5));
        d.y = h *(0.5 + 0.5* (Math.random()-0.5));
    });

*/
  force
      .size([w,h])
      .gravity(0)
      .nodes(nodes)
      .links(links)
      .linkDistance(linkDistance)
      .start();

  var link = svg.selectAll("line")
      .data(links)
    .enter().append("svg:line")
      .attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; });

  var node = svg.selectAll("g")
      .data(nodes)
    .enter().append("svg:g")
        // draw each region in a coord system centered at its centroid
      .attr("transform", function(d) { return "translate(" + -d[0] + "," + -d[1] + ")"; })
      .call(force.drag)
    .append("svg:path")
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
      .attr("d", function(d) { return path(d.feature); });

  node.append('title')
        .text(function(d) {return d.feature.properties.name;});
        
  force.on("tick", function(e) {
    link.attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node.attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });
  });
}

function edge(a, b) {
  var dx = a[0] - b[0], dy = a[1] - b[1];
  return {
    source: a,
    target: b,
    distance: Math.sqrt(dx * dx + dy * dy)
  };
}

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

d3.slider.css

.d3-slider {
    position: relative;
    font-family: Verdana,Arial,sans-serif;
    font-size: 1.1em;
    border: 1px solid #aaaaaa;
    z-index: 2;
}

.d3-slider-horizontal {
    height: .8em;
}  

.d3-slider-vertical {
    width: .8em;
    height: 100px;
}      

.d3-slider-handle {
    position: absolute;
    width: 1.2em;
    height: 1.2em;
    border: 1px solid #d3d3d3;
    border-radius: 4px;
    background: #eee;
    background: linear-gradient(to bottom, #eee 0%, #ddd 100%);
    z-index: 3;
}

.d3-slider-handle:hover {
    border: 1px solid #999999;
}

.d3-slider-horizontal .d3-slider-handle {
    top: -.3em;
    margin-left: -.6em;
}

.d3-slider-axis {
    position: relative;
    z-index: 1;    
}

.d3-slider-axis-bottom {
    top: .8em;
}

.d3-slider-axis-right {
    left: .8em;
}

.d3-slider-axis path {
    stroke-width: 0;
    fill: none;
}

.d3-slider-axis line {
    fill: none;
    stroke: #aaa;
    shape-rendering: crispEdges;
}

.d3-slider-axis text {
    font-size: 11px;
}

.d3-slider-vertical .d3-slider-handle {
    left: -.25em;
    margin-left: 0;
    margin-bottom: -.6em;      
}

d3.slider.js

/*
    D3.js Slider
    Inspired by jQuery UI Slider
    Copyright (c) 2013, Bjorn Sandvik - http://blog.thematicmapping.org
    BSD license: http://opensource.org/licenses/BSD-3-Clause
*/

d3.slider = function module() {
  "use strict";

  // Public variables width default settings
  var min = 0,
      max = 100,
      step = 1, 
      animate = true,
      orientation = "horizontal",
      axis = false,
      margin = 50,
      value,
      scale; 

  // Private variables
  var axisScale,
      dispatch = d3.dispatch("slide"),
      formatPercent = d3.format(".2%"),
      tickFormat = d3.format(".0"),
      sliderLength;

  function slider(selection) {
    selection.each(function() {

      // Create scale if not defined by user
      if (!scale) {
        scale = d3.scale.linear().domain([min, max]);
      }  

      // Start value
      value = value || scale.domain()[0]; 

      // DIV container
      var div = d3.select(this).classed("d3-slider d3-slider-" + orientation, true);

      var drag = d3.behavior.drag();

      // Slider handle
      var handle = div.append("a")
          .classed("d3-slider-handle", true)
          .attr("xlink:href", "#")
          .on("click", stopPropagation)
          .call(drag);

      // Horizontal slider
      if (orientation === "horizontal") {

        div.on("click", onClickHorizontal);
        drag.on("drag", onDragHorizontal);
        handle.style("left", formatPercent(scale(value)));
        sliderLength = parseInt(div.style("width"), 10);

      } else { // Vertical

        div.on("click", onClickVertical);
        drag.on("drag", onDragVertical);
        handle.style("bottom", formatPercent(scale(value)));
        sliderLength = parseInt(div.style("height"), 10);

      }
      
      if (axis) {
        createAxis(div);
      }


      function createAxis(dom) {

        // Create axis if not defined by user
        if (typeof axis === "boolean") {

          axis = d3.svg.axis()
              .ticks(Math.round(sliderLength / 100))
              .tickFormat(tickFormat)
              .orient((orientation === "horizontal") ? "bottom" :  "right");

        }      

        // Copy slider scale to move from percentages to pixels
        axisScale = scale.copy().range([0, sliderLength]);
          axis.scale(axisScale);

          // Create SVG axis container
        var svg = dom.append("svg")
            .classed("d3-slider-axis d3-slider-axis-" + axis.orient(), true)
            .on("click", stopPropagation);

        var g = svg.append("g");

        // Horizontal axis
        if (orientation === "horizontal") {

          svg.style("left", -margin);

          svg.attr({ 
            width: sliderLength + margin * 2, 
            height: margin
          });  

          if (axis.orient() === "top") {
            svg.style("top", -margin);  
            g.attr("transform", "translate(" + margin + "," + margin + ")")
          } else { // bottom
            g.attr("transform", "translate(" + margin + ",0)")
          }

        } else { // Vertical

          svg.style("top", -margin);

          svg.attr({ 
            width: margin, 
            height: sliderLength + margin * 2
          });      

          if (axis.orient() === "left") {
            svg.style("left", -margin);
            g.attr("transform", "translate(" + margin + "," + margin + ")")  
          } else { // right          
            g.attr("transform", "translate(" + 0 + "," + margin + ")")      
          }

        }

        g.call(axis);  

      }


      // Move slider handle on click/drag
      function moveHandle(pos) {

        var newValue = stepValue(scale.invert(pos / sliderLength));

        if (value !== newValue) {
          var oldPos = formatPercent(scale(stepValue(value))),
              newPos = formatPercent(scale(stepValue(newValue))),
              position = (orientation === "horizontal") ? "left" : "bottom";

          dispatch.slide(d3.event.sourceEvent || d3.event, value = newValue);

          if (animate) {
            handle.transition()
                .styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
                .duration((typeof animate === "number") ? animate : 250);
          } else {
            handle.style(position, newPos);          
          }
        }

      }


      // Calculate nearest step value
      function stepValue(val) {

        var valModStep = (val - scale.domain()[0]) % step,
            alignValue = val - valModStep;

        if (Math.abs(valModStep) * 2 >= step) {
          alignValue += (valModStep > 0) ? step : -step;
        }

        return alignValue;

      }


      function onClickHorizontal() {
        moveHandle(d3.event.offsetX || d3.event.layerX);
      }

      function onClickVertical() {
        moveHandle(sliderLength - d3.event.offsetY || d3.event.layerY);
      }

      function onDragHorizontal() {
        moveHandle(Math.max(0, Math.min(sliderLength, d3.event.x)));
      }

      function onDragVertical() {
        moveHandle(sliderLength - Math.max(0, Math.min(sliderLength, d3.event.y)));
      }      

      function stopPropagation() {
        d3.event.stopPropagation();
      }

    });

  } 

  // Getter/setter functions
  slider.min = function(_) {
    if (!arguments.length) return min;
    min = _;
    return slider;
  } 

  slider.max = function(_) {
    if (!arguments.length) return max;
    max = _;
    return slider;
  }     

  slider.step = function(_) {
    if (!arguments.length) return step;
    step = _;
    return slider;
  }   

  slider.animate = function(_) {
    if (!arguments.length) return animate;
    animate = _;
    return slider;
  } 

  slider.orientation = function(_) {
    if (!arguments.length) return orientation;
    orientation = _;
    return slider;
  }     

  slider.axis = function(_) {
    if (!arguments.length) return axis;
    axis = _;
    return slider;
  }     

  slider.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return slider;
  }  

  slider.value = function(_) {
    if (!arguments.length) return value;
    value = _;
    return slider;
  }  

  slider.scale = function(_) {
    if (!arguments.length) return scale;
    scale = _;
    return slider;
  }  

  d3.rebind(slider, dispatch, "on");

  return slider;

}