block by emeeks 302096884d5fbc1817062492605b50dd

D3v4 Constraint-Based Layout

Full Screen

Organizing a network made of disconnected pieces like this one is improved by fixing those subgraphs into their own space on the canvas. A while back I showed how to do this using custom code in the tick() function of the D3v3 force layout as part of How to Create Effective Network Data Visualization.

Here’s a much easier and cleaner way to do it using the new D3v4 force-layout by specifying a d3.forceY and d3.forceX that are associated with the node’s module.

index.html

<html>
<head>
  <title>d3v4 Constraint-based Layout</title>
  <meta charset="utf-8" />
<script src="https://d3js.org/d3.v4.0.0-alpha.33.min.js"></script>
</head>
<style>
  svg {
    height: 500px;
    width: 500px;
    border: 1px solid gray;
  }
</style>
<body>

<div id="viz">
  <svg class="main">
  </svg>
</div>
</body>
  <footer>
<script>

d3.json("graph.json",function(error,data) {createNetwork(data)});

function onlyUnique(value, index, self) {
  return self.indexOf(value) === index;
}

function createNetwork(data) {

  var edges = data.edges;
  var nodes = data.nodes;

//create a network from an edgelist

var colors = ["#996666", "#66CCCC", "#FFFF99", "#CC9999", "#666633", "#993300", "#999966", "#660000", "#996699", "#cc6633", "#ff9966", "#339999", "#6699cc", "#ffcc66", "#ff6600", "#00ccccc"]

//This isn't "gravity" it's the visual centering of the network based on its mass
var networkCenter = d3.forceCenter().x(250).y(250);

//CHARGE
var manyBody = d3.forceManyBody().strength(-150).distanceMax(100)

//Specify module position for the three largest modules. This is the x-y center of the modules
//singletons and small modules will be handled as a whole
var modulePosition = {
  "2": {x: 0, y: 0},
  "3": {x: 200, y: 25},
  "1": {x: 0, y: 200}
}

//Make the x-position equal to the x-position specified in the module positioning object or, if not in
//the hash, then set it to 250
var forceX = d3.forceX(function (d) {return modulePosition[d.module] ? modulePosition[d.module].x : 250})
    .strength(0.05)

//Same for forceY--these act as a gravity parameter so the different strength determines how closely
//the individual nodes are pulled to the center of their module position
var forceY = d3.forceY(function (d) {return modulePosition[d.module] ? modulePosition[d.module].y : 250})
    .strength(0.05)

  var force = d3.forceSimulation(nodes)
    .force("charge", manyBody)
    .force("link", d3.forceLink(edges).distance(30).iterations(1))
    .force("center", networkCenter)
    .force("x", forceX)
    .force("y", forceY)
    .on("tick", updateNetwork);

  var edgeEnter = d3.select("svg.main").selectAll("g.edge")
  .data(edges)
  .enter()
  .append("g")
  .attr("class", "edge");

  edgeEnter
  .append("line")
  .style("stroke-width", function (d) {return d.border ? "3px" : "1px"})
  .style("stroke", "black")
  .style("pointer-events", "none");

  var nodeEnter = d3.select("svg.main").selectAll("g.node")
  .data(nodes, function (d) {return d.id})
  .enter()
  .append("g")
  .attr("class", "node")

  nodeEnter.append("circle")
  .attr("r", 8)
  .style("fill", function (d) {return colors[d.module]})
  .style("stroke", "black")
  .style("stroke-width", function (d) {return d.border ? "3px" : "1px"})

  nodeEnter.append("text")
  .style("text-anchor", "middle")
  .attr("y", 3)
  .style("stroke-width", "1px")
  .style("stroke-opacity", 0.75)
  .style("stroke", "white")
  .style("font-size", "8px")
  .text(function (d) {return d.id})
  .style("pointer-events", "none")

  nodeEnter.append("text")
  .style("text-anchor", "middle")
  .attr("y", 3)
  .style("font-size", "8px")
  .text(function (d) {return d.id})
  .style("pointer-events", "none")

//  force.start();

  function updateNetwork(e) {
    d3.select("svg.main").selectAll("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});

    d3.select("svg.main").selectAll("g.node")
      .attr("transform", function (d) {return "translate(" + d.x + "," + d.y + ")"});

  }

}
</script>
  </footer>

</html>

graph.json

{"nodes": [{"id":"1","module":0,"i":0},{"id":"3","module":0,"i":1},{"id":"8","module":2,"i":2},{"id":"9","module":4,"i":3},{"id":"12","module":2,"i":4},{"id":"15","module":2,"i":5},{"id":"23","module":3,"i":6},{"id":"26","module":3,"i":7},{"id":"37","module":10,"i":8},{"id":"46","module":2,"i":9},{"id":"2","module":1,"i":10},{"id":"4","module":2,"i":11},{"id":"5","module":3,"i":12},{"id":"6","module":3,"i":13},{"id":"7","module":1,"i":14},{"id":"10","module":4,"i":15},{"id":"11","module":1,"i":16},{"id":"13","module":1,"i":17},{"id":"14","module":1,"i":18},{"id":"16","module":5,"i":19},{"id":"18","module":1,"i":20},{"id":"19","module":3,"i":21},{"id":"20","module":1,"i":22},{"id":"21","module":6,"i":23},{"id":"22","module":2,"i":24},{"id":"25","module":3,"i":25},{"id":"27","module":1,"i":26},{"id":"28","module":3,"i":27},{"id":"29","module":1,"i":28},{"id":"31","module":2,"i":29},{"id":"33","module":2,"i":30},{"id":"34","module":9,"i":31},{"id":"35","module":1,"i":32},{"id":"38","module":2,"i":33},{"id":"41","module":4,"i":34},{"id":"32","module":2,"i":35},{"id":"40","module":2,"i":36},{"id":"17","module":4,"i":37},{"id":"36","module":3,"i":38},{"id":"39","module":3,"i":39},{"id":"44","module":3,"i":40},{"id":"45","module":3,"i":41},{"id":"42","module":9,"i":42},{"id":"43","module":11,"i":43},{"id":"24","module":7,"i":44},{"id":"30","module":8,"i":45}],
"edges": [{"source":0,"target":1},{"source":10,"target":14},{"source":10,"target":16},{"source":10,"target":22},{"source":10,"target":28},{"source":10,"target":32},{"source":11,"target":36},{"source":12,"target":13},{"source":12,"target":21},{"source":12,"target":7},{"source":12,"target":41},{"source":13,"target":25},{"source":13,"target":7},{"source":13,"target":27},{"source":13,"target":38},{"source":13,"target":39},{"source":13,"target":40},{"source":14,"target":10},{"source":14,"target":18},{"source":14,"target":32},{"source":2,"target":29},{"source":2,"target":30},{"source":3,"target":37},{"source":15,"target":3},{"source":15,"target":37},{"source":16,"target":10},{"source":4,"target":24},{"source":4,"target":30},{"source":4,"target":9},{"source":37,"target":15},{"source":20,"target":17},{"source":21,"target":12},{"source":21,"target":13},{"source":21,"target":7},{"source":21,"target":38},{"source":21,"target":39},{"source":21,"target":40},{"source":21,"target":41},{"source":22,"target":17},{"source":24,"target":4},{"source":24,"target":5},{"source":24,"target":30},{"source":24,"target":36},{"source":6,"target":38},{"source":25,"target":13},{"source":7,"target":12},{"source":7,"target":13},{"source":7,"target":21},{"source":7,"target":6},{"source":7,"target":25},{"source":7,"target":38},{"source":7,"target":39},{"source":7,"target":40},{"source":7,"target":41},{"source":26,"target":17},{"source":26,"target":32},{"source":27,"target":13},{"source":29,"target":2},{"source":35,"target":11},{"source":35,"target":36},{"source":30,"target":11},{"source":30,"target":2},{"source":30,"target":4},{"source":30,"target":24},{"source":30,"target":36},{"source":31,"target":42},{"source":38,"target":13},{"source":38,"target":6},{"source":38,"target":7},{"source":38,"target":40},{"source":38,"target":41},{"source":33,"target":24},{"source":39,"target":13},{"source":39,"target":21},{"source":39,"target":41},{"source":36,"target":11},{"source":36,"target":35},{"source":36,"target":30},{"source":34,"target":15},{"source":40,"target":13},{"source":40,"target":21},{"source":41,"target":12},{"source":41,"target":13},{"source":41,"target":7},{"source":41,"target":38},{"source":9,"target":4}]
}