how to update foci dynamically in multi-foci force-layout in d3.js

泪湿孤枕 提交于 2020-01-05 12:31:16

问题


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 familiarity, which would change the chart from 3 clusters of bubbles to 5 clusters of bubbles. Current foci are hard-coded, which prevent toggling from working.

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 = [],
    foci = [{x: 0, y: 150}, {x: 400, y: 150}, {x: 200, y: 150}];

var svg = d3.select("body").append("svg")
    .attr("width", "100%")
    .attr("height", height)
    //.attr("domflag", '');

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;

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

  // Push nodes toward their designated focus.
  nodes.forEach(function(o, i) {
    o.y += (foci[o.id].y - o.y) * k;
    o.x += (foci[o.id].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});
  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="//d3js.org/d3.v3.min.js"></script>

How can I dynamically set the coordinates of the foci such that if it's just 3-4 cluster, align it in one row, but if it's 10 cluster, make it a 3-row small multiples?

Thanks.


回答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>


来源:https://stackoverflow.com/questions/49883063/how-to-update-foci-dynamically-in-multi-foci-force-layout-in-d3-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!