如何在d3.js中的multi-foci force-layout中动态更新焦点

时间:2018-04-17 16:22:24

标签: javascript d3.js force-layout

我有一个多焦点布局,无法找到动态设置焦点的方法。

在下面的代码中使用数据子集,我希望能够在id-group和熟悉度之间切换,这会将图表从3个气泡簇更改为5个气泡簇。目前的焦点是硬编码的,可防止切换工作。



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>
&#13;
&#13;
&#13;

如何动态设置焦点的坐标,如果它只有3-4个簇,则将其对齐成一行,但如果它是10个簇,则将其设为3行小倍数?

感谢。

1 个答案:

答案 0 :(得分:2)

您最重要的变化是修改刻度功能,以便选择一组焦点或另一组。

然而,首先,我们需要跟踪当前正在使用的焦点。所有这一切都需要在&#34; family&#34;之间切换。和#34;熟悉&#34;或者不太直观的东西,如你想要的真假。我在下面的代码中使用了变量current

现在我们可以通过添加某种检查来添加到您现有的tick功能,以查看应该使用哪些焦点集:

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 + ")"; });
}

我将数组焦点重命名为familyFoci,因为两个焦点都可以描述任一个数组,我还确保您的节点在下面的片段中具有熟悉属性

此修改允许我们轻松指定用于在一组焦点中设置特定焦点的属性,并指定我们想要的焦点集。

现在我们可以创建第二组焦点:

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}];

为了完整起见,我添加了一组基本按钮,这些按钮使用onclick功能来检查所需的焦点集是什么。

以下是快速摘录中的所有内容:

&#13;
&#13;
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);
&#13;
circle {
  stroke: #fff;
}
&#13;
<script src="https://d3js.org/d3.v3.min.js"></script>
&#13;
&#13;
&#13;

单击一个选项,如果它不是当前选定的焦点,则力会改变它正在使用的焦点。

但是,这里有一个问题,图表继续降温,因为你转移焦点直到它最终停止。当我们点击其中一个按钮时,我们可以稍微润滑一些轮子并用另外一行代码重置温度(alpha):

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

这是一个演示:

&#13;
&#13;
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);
&#13;
circle {
  stroke: #fff;
}
&#13;
<script src="https://d3js.org/d3.v3.min.js"></script>
&#13;
&#13;
&#13;