Skip to content Skip to sidebar Skip to footer

How To Update Foci Dynamically In Multi-foci Force-layout In D3.js

I have a multi-foci layout and couldn't find a way to dynamically set the foci. In the code below using a subset of data, I wish to be able to toggle between id-group and familiari

Solution 1:

You most important change here is modifying the tick function to give the option of selecting one set of foci or the other.

First, however, we need to keep track of which foci points are currently being used. All this needs to do is toggle between "family" and "familiarity" or something less intuitive such as true or false if you want. I've used the variable current in the code below.

Now we can add to your existing tick function by adding some sort of check to see what set of foci should be used:

function tick(e) {
  var k = .3 * e.alpha;

  // nudge nodes to proper foci:
  if(current == "family" ) {
    nodes.forEach(function(o, i) {
      o.y += (familyFoci[o.id].y - o.y) * k;
      o.x += (familyFoci[o.id].x - o.x) * k;
    });
  }
  else {
     nodes.forEach(function(o, i) {
      o.y += (familiarityFoci[o.familiarity].y - o.y) * k;
      o.x += (familiarityFoci[o.familiarity].x - o.x) * k;
    }); 

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

I renamed the array foci to familyFoci as both foci could describe either array, I also made sure that your nodes have a familiarity property in the snippets below

This modification allows us to easily specify the property used to set a specific focal point in a set of focal points, and specify which set of focal points we want.

Now we can create a second set of foci:

var familyFoci = [{x: 0, y: 150}, {x: 400, y: 150}, {x: 200, y: 150}];
var familiarityFoci = [{x:0,y:200},{x:100,y:100},{x:200,y:200},{x:300,y:100},{x:400,y:200}];

For the sake of completeness I've added a basic set of buttons that use an onclick function to check to see what the desired set of focal points is.

Here's all that in a quick snippet:

var data = [
  {"id": 0, "name": "AngularJS", "familiarity":0,"r": 50 },
  {"id": 0, "name": "HTML5", "familiarity":1,"r": 40 },
  {"id": 0, "name": "Javascript", "familiarity":2,"r": 30 },


  {"id": 1, "name": "Actionscript","familiarity":0, "r": 50 },
  {"id": 1, "name": "Flash", "familiarity":4, "r": 32 },


  {"id": 2, "name": "Node Webkit", "familiarity":3,"r": 40 },
  {"id": 2, "name": "Chrome App", "familiarity":3,"r": 30 },
  {"id": 2, "name": "Cordova", "familiarity":0,"r": 45 },
];

var width = window.innerWidth,
    height = 450;

var fill = d3.scale.category10();

var nodes = [], labels = [];
    
// two sets of foci:
var familyFoci = [{x: 0, y: 150}, {x: 400, y: 150}, {x: 200, y: 150}];
var familiarityFoci = [{x:0,y:200},{x:100,y:100},{x:200,y:200},{x:300,y:100},{x:400,y:200}];
	
	
var svg = d3.select("body").append("svg")
    .attr("width", "100%")
    .attr("height", height)

var force = d3.layout.force()
    .nodes(nodes)
    .links([])
    .charge(-200)
    .gravity(0.1)
    .friction(0.8)
    .size([width, height])
    .on("tick", tick);
	
//var node = svg.selectAll("circle");
var node = svg.selectAll("g");

var counter = 0;

//
// Create a basic interface:
//
var current = "family";
var buttons = svg.selectAll(null)
  .data(["family","familiarity"])
  .enter()
  .append("g")
  .attr("transform",function(d,i)  { return "translate("+(i*120+50)+","+50+")"; })
  .on("click", function(d) {
    if(d != current) {
	  current = d;
	} 
  })
  .style("cursor","pointer")
  
buttons.append("rect")
  .attr("width",100)
  .attr("height",50)
  .attr("fill","lightgrey")
    
buttons.append("text")
  .text(function(d) { return d; })
  .attr("dy", 30)
  .attr("dx", 50)
  .style("text-anchor","middle");

  

function tick(e) {
  var k = .3 * e.alpha;

  //
  // Check to see what foci set we should gravitate to:
  //
  if(current == "family") {
    // Push nodes toward their designated focus.
    nodes.forEach(function(o, i) {
      o.y += (familyFoci[o.id].y - o.y) * k;
      o.x += (familyFoci[o.id].x - o.x) * k;
    });
  }
  else {
     nodes.forEach(function(o, i) {
      o.y += (familiarityFoci[o.familiarity].y - o.y) * k;
      o.x += (familiarityFoci[o.familiarity].x - o.x) * k;
    }); 
  
  }

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

}





var timer = setInterval(function(){

  if (nodes.length > data.length-1) { clearInterval(timer); return;}

  var item = data[counter];
  nodes.push({id: item.id, r: item.r, name: item.name, familiarity: item.familiarity});
  force.start();

  node = node.data(nodes);

  var n = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
      .style('cursor', 'pointer')
      .on('mousedown', function() {
         var sel = d3.select(this);
         sel.moveToFront();
      })
      .call(force.drag);

  n.append("circle")
      .attr("r",  function(d) { return d.r/2; })
      .style("fill", function(d) { return fill(d.id); })

  n.append("text")
      .text(function(d){
          return d.name;
      })
      .style("font-size", function(d) {
          return Math.min(2 * d.r, (2 * d.r - 8) / this.getComputedTextLength() * 16) + "px"; 
       })
      .attr("dy", ".35em")

  counter++;
}, 100);


d3.selection.prototype.moveToFront = function() {
  return this.each(function(){
    this.parentNode.appendChild(this);
  });
};

function resize() {
  width = window.innerWidth;
  force.size([width, height]);
  force.start();
}

d3.select(window).on('resize', resize);
circle {
  stroke: #fff;
}
<script src="https://d3js.org/d3.v3.min.js"></script>

Click on one option, and if it isn't the currently selected foci, the force changes which foci it is using.

But, there is a problem here, the graph continues to cool down as you shift the foci until it ultimately stops. We can grease the wheels a bit and reset the temperature (alpha) with one more line of code when we click on one of our buttons:

  .on("click", function(d) {
    if(d != current) {
      current = d;
    force.alpha(0.228);  // reset the alpha
      } 
  })

And here's a demo:

var data = [
  {"id": 0, "name": "AngularJS", "familiarity":0,"r": 50 },
  {"id": 0, "name": "HTML5", "familiarity":1,"r": 40 },
  {"id": 0, "name": "Javascript", "familiarity":2,"r": 30 },


  {"id": 1, "name": "Actionscript","familiarity":0, "r": 50 },
  {"id": 1, "name": "Flash", "familiarity":4, "r": 32 },


  {"id": 2, "name": "Node Webkit", "familiarity":3,"r": 40 },
  {"id": 2, "name": "Chrome App", "familiarity":3,"r": 30 },
  {"id": 2, "name": "Cordova", "familiarity":0,"r": 45 },
];

var width = window.innerWidth,
    height = 450;

var fill = d3.scale.category10();

var nodes = [], labels = [];
    
// two sets of foci:
var familyFoci = [{x: 0, y: 150}, {x: 400, y: 150}, {x: 200, y: 150}];
var familiarityFoci = [{x:0,y:200},{x:100,y:100},{x:200,y:200},{x:300,y:100},{x:400,y:200}];
	
	
var svg = d3.select("body").append("svg")
    .attr("width", "100%")
    .attr("height", height)

var force = d3.layout.force()
    .nodes(nodes)
    .links([])
    .charge(-200)
    .gravity(0.1)
    .friction(0.8)
    .size([width, height])
    .on("tick", tick);
	
var node = svg.selectAll("g");

var counter = 0;

//
// Create a basic interface:
//
var current = "family";
var buttons = svg.selectAll(null)
  .data(["family","familiarity"])
  .enter()
  .append("g")
  .attr("transform",function(d,i)  { return "translate("+(i*120+50)+","+50+")"; })
  .on("click", function(d) {
    if(d != current) {
	  current = d;
    force.alpha(0.228);
	  } 
  })
  .style("cursor","pointer")
  
buttons.append("rect")
  .attr("width",100)
  .attr("height",50)
  .attr("fill","lightgrey")
    
buttons.append("text")
  .text(function(d) { return d; })
  .attr("dy", 30)
  .attr("dx", 50)
  .style("text-anchor","middle");


function tick(e) {
  var k = .3 * e.alpha;

  //
  // Check to see what foci set we should gravitate to:
  //
  if(current == "family") {
    // Push nodes toward their designated focus.
    nodes.forEach(function(o, i) {
      o.y += (familyFoci[o.id].y - o.y) * k;
      o.x += (familyFoci[o.id].x - o.x) * k;
    });
  }
  else {
     nodes.forEach(function(o, i) {
      o.y += (familiarityFoci[o.familiarity].y - o.y) * k;
      o.x += (familiarityFoci[o.familiarity].x - o.x) * k;
    }); 
  
  }

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

}





var timer = setInterval(function(){

  if (nodes.length > data.length-1) { clearInterval(timer); return;}

  var item = data[counter];
  nodes.push({id: item.id, r: item.r, name: item.name, familiarity: item.familiarity});
  force.start();

  node = node.data(nodes);

  var n = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
      .style('cursor', 'pointer')
      .on('mousedown', function() {
         var sel = d3.select(this);
         sel.moveToFront();
      })
      .call(force.drag);

  n.append("circle")
      .attr("r",  function(d) { return d.r/2; })
      .style("fill", function(d) { return fill(d.id); })

  n.append("text")
      .text(function(d){
          return d.name;
      })
      .style("font-size", function(d) {
          return Math.min(2 * d.r, (2 * d.r - 8) / this.getComputedTextLength() * 16) + "px"; 
       })
      .attr("dy", ".35em")

  counter++;
}, 100);


d3.selection.prototype.moveToFront = function() {
  return this.each(function(){
    this.parentNode.appendChild(this);
  });
};

function resize() {
  width = window.innerWidth;
  force.size([width, height]);
  force.start();
}

d3.select(window).on('resize', resize);
circle {
  stroke: #fff;
}
<script src="https://d3js.org/d3.v3.min.js"></script>

Post a Comment for "How To Update Foci Dynamically In Multi-foci Force-layout In D3.js"