javascript - 如何在 d3.js 的多焦点强制布局中动态更新焦点

标签 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>

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

谢谢。

最佳答案

此处最重要的更改是修改 tick 函数以提供选择一组焦点或另一组焦点的选项。

但是,首先,我们需要跟踪当前正在使用的焦点。所有这一切需要做的就是在“家庭”和“熟悉度”之间切换,或者如果你愿意,可以选择一些不太直观的东西,比如真或假。我在下面的代码中使用了变量 current

现在我们可以通过添加某种检查来查看应该使用哪组焦点来添加到您现有的刻度函数:

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

我将数组 foci 重命名为 familyFoci,因为两个 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}];

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

这是一个简短的片段:

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>

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

但是,这里有一个问题,当您移动焦点时,图表会继续冷却,直到它最终停止。当我们点击其中一个按钮时,我们可以稍微润滑一下轮子并用一行代码重置温度 (alpha):

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

这是一个演示:

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>

关于javascript - 如何在 d3.js 的多焦点强制布局中动态更新焦点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49883063/

相关文章:

javascript - 如何在 svg 矩形中打包文本

javascript - 迭代地将链接附加到 D3 力定向网络可视化

d3.js - 传递给d3.js强制刻度回调函数的 `e`参数是什么?

javascript - canvas.getContext ('2D' ) 返回空值

javascript - 使用 date toLocaleString 时,日期时间格式来自哪里?

JavaScriptCore 控制台.log

javascript - 从控制台中删除空行

javascript - D3.js 如何将力布局的节点安排在一个圆圈上

javascript - D3 树节点双击高亮文本

d3.js - 更改 d3 强制布局链接样式以匹配 d3-tree 外观