根据边界动态设置初始d3缩放-V4 [英] Set initial d3 zoom based on boundary dynamically - V4

查看:81
本文介绍了根据边界动态设置初始d3缩放-V4的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有很多节点要显示在页面上,大多数情况下,由于节点的位置,圆圈偏离了屏幕的可见区域。



是否可以根据节点的整个边界框动态设置初始缩放级别,以使所有节点都适合屏幕的可见区域?



已更新:



我为此

解决方案

在完成冷却之前,您无法预期力布局将占据的极限范围。但是,有两种可能的解决方案可以达到预期的效果。


  1. 约束布局,或者选择缩小


  2. 当结点冷却时冷却力超过svg的范围时,更改缩放比例。


第一个数字通过限制视口中的节点来达到相同的效果。但是,节点的大小不会缩小,这可能导致相当多的混乱。堆栈溢出有很多关于此方法的问题和答案(例如一个)。



我不相信我之前见过第二个例子。使用d3缩放功能应该不会太难。虽然我们无法在不运行的情况下预计布局的大小,但是我们可以根据力在任何给定时间点的大小进行动态缩放。为此,我们可以在很大程度上采用与缩放单个节点相同的方法:应用新的缩放标识。



但是,与缩放节点不同,我们需要确定规模。为了确定比例,我们需要找到力布局的边界并将其与svg的边界进行比较。我将使用与其他 answer 不同的方法,但是哪种方法都可以正常工作(我不确定哪种方法可以性能更高)。



首先,我们得到x和y coordiantes的范围:

  var xExtent = d3.extent(node.data(),function(d){return dx;}); 
var yExtent = d3.extent(node.data(),function(d){return d.y;});

我们也可以在此处容纳半径,我只是在使用节点中心作为答案



接下来,我们得到x和y的标度:

  var xScale = width /(xExtent [1] -xExtent [0]); 
var yScale = height /(yExtent [1] -yExtent [0]);

然后我们找出哪个更局限并使用该比例尺:

  var minScale = Math.min(xScale,yScale); 

现在,我们像缩放到一个点一样设置缩放标识,但是我们想要的点居中位于力布局的中间(我们可以使用刚计算出的范围来确定中间),而比例尺就是我们刚刚确定的比例尺。但是,我们仅在满足某些条件的情况下应用更改-在下面的示例中,将是节点超出svg的范围:

  if(minScale< 1){
var transform = d3.zoomIdentity.translate(width / 2,height / 2)
.scale(minScale)
.translate( -(xExtent [0] + xExtent [1])/ 2,-(yExtent [0] + yExtent [1])/ 2)
svg.call(zoom.transform,transform);
}

下面是此方法的演示,嵌入了tick函数:



  var svg = d3.select( svg), width = + svg.attr( width),height = + svg.attr( height); var zoom = d3.zoom().scaleExtent([-8/2,4]).on( zoom,zoomed); svg.call(zoom); var g = svg.append( g); var Simulation = d3.forceSimulation().force( link,d3.forceLink()。id(function(d){return d.id;})).force( charge,d3.forceManyBody())。 force( center,d3.forceCenter(width / 2,height / 2)); var graph = { nodes:[{ id: Myriel, group:1},{ id: Napoleon, group:1},{ id: Mile.Baptistine , group:1},{ id: Mme.Magloire, group:1},{ id: CountessdeLo, group:1},{ id: Geborand , group:1},{ id: Champtercier, group:1},{ id: Cravatte, group:1},{ id: Count,  group:1},{ id: OldMan, group:1},{ id: Labarre, group:2},{ id: Valjean, group :2},{ id:玛格丽特,组:3},{ id: Mme.deR,组:2},{ id: Isabeau,组:2},{ id: Gervais, group:2},{ id: Tholomyes, group:3},{ id: Listolier, group: 3},{ id: Fameuil, group:3},{ id: Blacheville, group:3},{ id:收藏ite, group:3},{ id:大丽花, group:3},{ id: Zephine, group:3},{ id: Fantine , group:3},{ id: Mme.Thenardier, group:4},{ id: Thenardier, group:4},{ id:珂赛特 , group:5},{ id: Javert, group:4},{ id: Fauchelevent, group:0},{ id: Bamatabois, group:2},{ id: Perpetue, group:3},{ id: Simplice, group:2},{ id: Scaufflaire, group :2},{ id: Woman1,小组:2},{ id:法官,小组:2},{ id: Champmathieu,小组:2 },{ id: Brevet, group:2},{ id: Chenildieu, group:2},{ id: Cochepaille, group:2}, { id: Pontmercy, group:4},{ id: Boulatruelle, group:6},{ id: E ponine, group:4},{ id: Anzelma, group:4},{ id: Woman2, group:5},{ id: MotherInnocent , group:0},{ id: Gribier, group:0},{ id: Jondrette, group:7},{ id: Mme.Burgon , group:7},{ id: Gavroche, group:8},{ id: Gillenormand, group:5},{ id: Magnon, group:5},{ id: Mlle.Gillenormand, group:5},{ id: Mme.Pontmercy, group:5},{ id: Mlle。 Vaubois, group:5},{ id: Lille.Gillenormand, group:5},{ id: Marius, group:8},{ id: BaronessT, group:5},{ id: Mabeuf, group:8},{ id: Enjolras, group:8},{ id: Combeferre , group:8},{ id: Prouvaire, group:8},{ id: Feuilly, group:8} ,{ id: Courfeyrac, group:8},{ id: Bahorel, group:8},{ id: Bossuet, group:8},{  id: Joly, group:8},{ id: Grantaire, group:8},{ id: MotherPlutarch, group:9},{ id : Gueulemer, group:4},{ id: Babet, group:4},{ id: Claquesous, group:4},{ id:  Montparnasse, group:4},{ id: Toussaint, group:5},{ id: Child1, group:10},{ id: Child2 , group:10},{ id: Brujon, group:4},{ id: Mme.Hucheloup, group:8}],链接:[{  source: Napoleon, target: Myriel, value:1},{ source: Mlle.Baptistine, target: Myriel, value:8},{ 源: Mme.Magloire,目标: Myriel,值:10}]} var链接= g.append( g) .attr( class, links).selectAll( line).data(graph.links).enter()。append( line); var node = g.append( g).attr( class, nodes).selectAll( circle).data(graph.nodes).enter()。append( circle).attr(  r,2.5).on('click',clicked); node.append( title).text(function(d){return d.id;});模拟.nodes(graph.nodes).on( tick,勾选); Simulation.force( link).links(graph.links); function ticked(){//设置缩放变换:var xExtent = d3.extent(node.data(),function(d){return d.x;}); var yExtent = d3.extent(node.data(),function(d){return d.y;}); //获取比例尺:var xScale = width /(xExtent [1]-xExtent [0]); var yScale = height /(yExtent [1]-yExtent [0]); //获得最严格的比例尺var minScale = Math.min(xScale,yScale); if(minScale< 1){var transform = d3.zoomIdentity.translate(width / 2,height / 2).scale(minScale).translate(-(xExtent [0] + xExtent [1])/ 2,-( yExtent [0] + yExtent [1])/ 2)svg.call(zoom.transform,transform); } link .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;});节点.attr( cx,function(d){return dx;}).attr( cy,function(d){return dy;}).attr('r',20)} var active = d3。 select(null); function clicked(d){if(active.node()=== this){active.classed( active,false);返回reset(); } active = d3.select(this).classed( active,true); svg.transition().duration(750).call(zoom.transform,d3.zoomIdentity .translate(width / 2,height / 2).scale(8).translate(-(+ active.attr('cx')) ),-(+ active.attr('cy')))); }函数reset(){svg.transition().duration(750).call(zoom.transform,d3.zoomIdentity .translate(0,0).scale(1)); }函数zoomed(){g.attr( transform,d3.event.transform); }  

 < script src = https:// d3js .org / d3.v5.min.js>< / script>< svg width = 960 height = 600>< / svg>  



上述问题是在模拟运行过程中鼠标事件实际上被忽略了-滴答事件运行得足够快,有效



有一些潜在的解决方案:




  • 在可视化效果冷却到足以使鼠标导航可用时停止自动缩放


  • 在启动用户缩放时停止自动缩放


  • 在力减弱之前不要启用用户缩放




我将在这里快速实现第一个,因为这可能是最简单的。我还将缩小缩放比例,使其具有一定的余量,以便在自动缩放停止时,节点仍应处于可见状态。在鼠标导航不会导致可见变化(从等待开始,更改为指针)的时间内,我还更改了光标:



  var svg = d3.select( svg),width = + svg.attr( width),height = + svg.attr (高度); var zoom = d3.zoom().scaleExtent([-8/2,4]).on( zoom,zoomed); svg.call(zoom); var g = svg.append( g); var Simulation = d3.forceSimulation().force( link,d3.forceLink()。id(function(d){return d.id;})).force( charge,d3.forceManyBody())。 force( center,d3.forceCenter(width / 2,height / 2)); var graph = { nodes:[{ id: Myriel, group:1},{ id: Napoleon, group:1},{ id: Mile.Baptistine , group:1},{ id: Mme.Magloire, group:1},{ id: CountessdeLo, group:1},{ id: Geborand , group:1},{ id: Champtercier, group:1},{ id: Cravatte, group:1},{ id: Count,  group:1},{ id: OldMan, group:1},{ id: Labarre, group:2},{ id: Valjean, group :2},{ id:玛格丽特,组:3},{ id: Mme.deR,组:2},{ id: Isabeau,组:2},{ id: Gervais, group:2},{ id: Tholomyes, group:3},{ id: Listolier, group: 3},{ id: Fameuil,小组:3},{ id: Blacheville,小组:3},{ id:赞成ite, group:3},{ id:大丽花, group:3},{ id: Zephine, group:3},{ id: Fantine , group:3},{ id: Mme.Thenardier, group:4},{ id: Thenardier, group:4},{ id:珂赛特 , group:5},{ id: Javert, group:4},{ id: Fauchelevent, group:0},{ id: Bamatabois, group:2},{ id: Perpetue, group:3},{ id: Simplice, group:2},{ id: Scaufflaire, group :2},{ id: Woman1,小组:2},{ id:法官,小组:2},{ id: Champmathieu,小组:2 },{ id: Brevet, group:2},{ id: Chenildieu, group:2},{ id: Cochepaille, group:2}, { id: Pontmercy, group:4},{ id: Boulatruelle, group:6},{ id: E ponine, group:4},{ id: Anzelma, group:4},{ id: Woman2, group:5},{ id: MotherInnocent , group:0},{ id: Gribier, group:0},{ id: Jondrette, group:7},{ id: Mme.Burgon , group:7},{ id: Gavroche, group:8},{ id: Gillenormand, group:5},{ id: Magnon, group:5},{ id: Mlle.Gillenormand, group:5},{ id: Mme.Pontmercy, group:5},{ id: Mlle。 Vaubois, group:5},{ id: Lille.Gillenormand, group:5},{ id: Marius, group:8},{ id: BaronessT, group:5},{ id: Mabeuf, group:8},{ id: Enjolras, group:8},{ id: Combeferre , group:8},{ id: Prouvaire, group:8},{ id: Feuilly, group:8} ,{ id: Courfeyrac, group:8},{ id: Bahorel, group:8},{ id: Bossuet, group:8},{  id: Joly, group:8},{ id: Grantaire, group:8},{ id: MotherPlutarch, group:9},{ id : Gueulemer, group:4},{ id: Babet, group:4},{ id: Claquesous, group:4},{ id:  Montparnasse, group:4},{ id: Toussaint, group:5},{ id: Child1, group:10},{ id: Child2 , group:10},{ id: Brujon, group:4},{ id: Mme.Hucheloup, group:8}],链接:[{  source: Napoleon, target: Myriel, value:1},{ source: Mlle.Baptistine, target: Myriel, value:8},{ 源: Mme.Magloire,目标: Myriel,值:10}]} var链接= g.append( g) .attr( class, links).selectAll( line).data(graph.links).enter()。append( line); var node = g.append( g).attr( class, nodes).selectAll( circle).data(graph.nodes).enter()。append( circle).attr(  r,2.5).on('click',clicked); node.append( title).text(function(d){return d.id;});模拟.nodes(graph.nodes).on( tick,勾选); Simulation.force( link).links(graph.links); var check = true; svg.attr( cursor, wait)函数tick(){if(this.alpha()> 0.04){//设置缩放变换:var xExtent = d3.extent(node.data(),function (d){return dx;}); var yExtent = d3.extent(node.data(),function(d){return d.y;}); //获取比例尺:var xScale = width /(xExtent [1]-xExtent [0])* 0.75; var yScale = height /(yExtent [1]-yExtent [0])* 0.75; //获得最严格的比例尺var minScale = Math.min(xScale,yScale); if(minScale< 1){var transform = d3.zoomIdentity.translate(width / 2,height / 2).scale(minScale).translate(-(xExtent [0] + xExtent [1])/ 2,-( yExtent [0] + yExtent [1])/ 2)svg.call(zoom.transform,transform); }} else {svg.attr( cursor, pointer)if(check)console.log( check); var check = false; } link .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;});节点.attr( cx,function(d){return dx;}).attr( cy,function(d){return dy;}).attr('r',20)} var active = d3。 select(null); function clicked(d){if(active.node()=== this){active.classed( active,false);返回reset(); } active = d3.select(this).classed( active,true); svg.transition().duration(750).call(zoom.transform,d3.zoomIdentity .translate(width / 2,height / 2).scale(8).translate(-(+ active.attr('cx')) ),-(+ active.attr('cy')))); }函数reset(){svg.transition().duration(750).call(zoom.transform,d3.zoomIdentity .translate(0,0).scale(1)); }函数zoomed(){g.attr( transform,d3.event.transform); }  

 < script src = https:// d3js .org / d3.v5.min.js>< / script>< svg width = 960 height = 600>< / svg>  



该力也只能在知道极限范围的一般概念后才能呈现,从而避免了导航结束的任何时间由自动缩放编写。


I have a large number of nodes to display on the page, most of the times the circles goes off the visible area of the screen due to node placements.

Is there a way to set the initial zoom level based on the whole boundary box of nodes dynamically so that all the nodes fits on the visible area of the screen?

Updated:

I have added a fiddle for this https://jsfiddle.net/navinleon/6ygaxoyq/3/

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");
    
    

  var zoom = d3.zoom()
    .scaleExtent([-8 / 2, 4])
    .on("zoom", zoomed);
    
    svg.call(zoom);

  var g = svg.append("g");

  var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

  var graph = {
    "nodes": [{
      "id": "Myriel",
      "group": 1
    }, {
      "id": "Napoleon",
      "group": 1
    }, {
      "id": "Mlle.Baptistine",
      "group": 1
    }, {
      "id": "Mme.Magloire",
      "group": 1
    }, {
      "id": "CountessdeLo",
      "group": 1
    }, {
      "id": "Geborand",
      "group": 1
    }, {
      "id": "Champtercier",
      "group": 1
    }, {
      "id": "Cravatte",
      "group": 1
    }, {
      "id": "Count",
      "group": 1
    }, {
      "id": "OldMan",
      "group": 1
    }, {
      "id": "Labarre",
      "group": 2
    }, {
      "id": "Valjean",
      "group": 2
    }, {
      "id": "Marguerite",
      "group": 3
    }, {
      "id": "Mme.deR",
      "group": 2
    }, {
      "id": "Isabeau",
      "group": 2
    }, {
      "id": "Gervais",
      "group": 2
    }, {
      "id": "Tholomyes",
      "group": 3
    }, {
      "id": "Listolier",
      "group": 3
    }, {
      "id": "Fameuil",
      "group": 3
    }, {
      "id": "Blacheville",
      "group": 3
    }, {
      "id": "Favourite",
      "group": 3
    }, {
      "id": "Dahlia",
      "group": 3
    }, {
      "id": "Zephine",
      "group": 3
    }, {
      "id": "Fantine",
      "group": 3
    }, {
      "id": "Mme.Thenardier",
      "group": 4
    }, {
      "id": "Thenardier",
      "group": 4
    }, {
      "id": "Cosette",
      "group": 5
    }, {
      "id": "Javert",
      "group": 4
    }, {
      "id": "Fauchelevent",
      "group": 0
    }, {
      "id": "Bamatabois",
      "group": 2
    }, {
      "id": "Perpetue",
      "group": 3
    }, {
      "id": "Simplice",
      "group": 2
    }, {
      "id": "Scaufflaire",
      "group": 2
    }, {
      "id": "Woman1",
      "group": 2
    }, {
      "id": "Judge",
      "group": 2
    }, {
      "id": "Champmathieu",
      "group": 2
    }, {
      "id": "Brevet",
      "group": 2
    }, {
      "id": "Chenildieu",
      "group": 2
    }, {
      "id": "Cochepaille",
      "group": 2
    }, {
      "id": "Pontmercy",
      "group": 4
    }, {
      "id": "Boulatruelle",
      "group": 6
    }, {
      "id": "Eponine",
      "group": 4
    }, {
      "id": "Anzelma",
      "group": 4
    }, {
      "id": "Woman2",
      "group": 5
    }, {
      "id": "MotherInnocent",
      "group": 0
    }, {
      "id": "Gribier",
      "group": 0
    }, {
      "id": "Jondrette",
      "group": 7
    }, {
      "id": "Mme.Burgon",
      "group": 7
    }, {
      "id": "Gavroche",
      "group": 8
    }, {
      "id": "Gillenormand",
      "group": 5
    }, {
      "id": "Magnon",
      "group": 5
    }, {
      "id": "Mlle.Gillenormand",
      "group": 5
    }, {
      "id": "Mme.Pontmercy",
      "group": 5
    }, {
      "id": "Mlle.Vaubois",
      "group": 5
    }, {
      "id": "Lt.Gillenormand",
      "group": 5
    }, {
      "id": "Marius",
      "group": 8
    }, {
      "id": "BaronessT",
      "group": 5
    }, {
      "id": "Mabeuf",
      "group": 8
    }, {
      "id": "Enjolras",
      "group": 8
    }, {
      "id": "Combeferre",
      "group": 8
    }, {
      "id": "Prouvaire",
      "group": 8
    }, {
      "id": "Feuilly",
      "group": 8
    }, {
      "id": "Courfeyrac",
      "group": 8
    }, {
      "id": "Bahorel",
      "group": 8
    }, {
      "id": "Bossuet",
      "group": 8
    }, {
      "id": "Joly",
      "group": 8
    }, {
      "id": "Grantaire",
      "group": 8
    }, {
      "id": "MotherPlutarch",
      "group": 9
    }, {
      "id": "Gueulemer",
      "group": 4
    }, {
      "id": "Babet",
      "group": 4
    }, {
      "id": "Claquesous",
      "group": 4
    }, {
      "id": "Montparnasse",
      "group": 4
    }, {
      "id": "Toussaint",
      "group": 5
    }, {
      "id": "Child1",
      "group": 10
    }, {
      "id": "Child2",
      "group": 10
    }, {
      "id": "Brujon",
      "group": 4
    }, {
      "id": "Mme.Hucheloup",
      "group": 8
    }],
    "links": [{
      "source": "Napoleon",
      "target": "Myriel",
      "value": 1
    }, {
      "source": "Mlle.Baptistine",
      "target": "Myriel",
      "value": 8
    }, {
      "source": "Mme.Magloire",
      "target": "Myriel",
      "value": 10
    }]
  }

  var link = g.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line");

  var node = g.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", 2.5)
    .on('click', clicked);

  node.append("title")
    .text(function(d) {
      return d.id;
    });

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

  simulation.force("link")
    .links(graph.links);

  function ticked() {
    link
      .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;
      });

    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr('r',20)
  }

  var active = d3.select(null);

  function clicked(d) {

    if (active.node() === this){
      active.classed("active", false);
      return reset();
    }
    
    active = d3.select(this).classed("active", true);

    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(8)
        .translate(-(+active.attr('cx')), -(+active.attr('cy')))
      );
  }

  function reset() {
    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(0, 0)
        .scale(1)
      );
  }

  function zoomed() {
    g.attr("transform", d3.event.transform);
  }

<script src="https://d3js.org/d3.v5.min.js"></script>

<svg width="960" height="600"></svg>

Expected:

解决方案

You can't anticipate the ultimate bounds the force layout will occupy until it finishes cooling down. However, there are two possible solutions that might achieve the desired effect.

  1. Constrain the layout, or alternatively explore reducing forces and velocities as nodes approach the bounds of the svg.

  2. Change the zoom as the force expands beyond the bounds of the svg while cooling down.

Number one achieves the same effect by bounding the nodes in the viewport. However, nodes won't be scaled down in size, which could lead to a fair amount of clutter. There are a number of questions and answers on stack overflow that deal with this approach (such as this one).

I don't believe I've seen an example of number two before. This shouldn't be too hard to do using the d3 scale functionality. While we can't anticipate the size of the layout without running it, we can zoom dynamically based on the size of the force at any given point in time. To do so we can largely adopt the same approach you use to zoom to an individual node: apply a new zoom identity.

However, unlike when zooming to a node, we need to determine the scale. To determine the scale we need find the bounds of the force layout and compare it with the bounds of the svg. I'll use a different approach than the other answer, but either approach should work fine (I'm not certain which is more performant).

First we get the extent of x and y coordiantes:

 var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });

We could accomodate the radius here too, I'm just using node center for this answer

Next we get the scales for x and y:

var xScale = width/(xExtent[1]-xExtent[0]);
var yScale = height/(yExtent[1]-yExtent[0]);

And then we find out which is more confining and use that scale:

var minScale = Math.min(xScale,yScale);

Now we set the zoom identity just as we would when zooming to a point, but the point we want to center on is the middle of the force layout (we can use the extents we just calculated to determine the middle), and the scale is the scale we just determined. However, we only apply the change if some condition is met - in my example below, it will be if the nodes exceed the bounds of the svg:

if(minScale < 1) {
   var transform = d3.zoomIdentity.translate(width/2,height/2)
    .scale(minScale)
    .translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
  svg.call(zoom.transform, transform);
}

Below is a demonstration of this approach, embedded in the tick function:

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  var zoom = d3.zoom()
    .scaleExtent([-8 / 2, 4])
    .on("zoom", zoomed);
    
    svg.call(zoom);

  var g = svg.append("g");

  var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

  var graph = {
    "nodes": [{
      "id": "Myriel",
      "group": 1
    }, {
      "id": "Napoleon",
      "group": 1
    }, {
      "id": "Mlle.Baptistine",
      "group": 1
    }, {
      "id": "Mme.Magloire",
      "group": 1
    }, {
      "id": "CountessdeLo",
      "group": 1
    }, {
      "id": "Geborand",
      "group": 1
    }, {
      "id": "Champtercier",
      "group": 1
    }, {
      "id": "Cravatte",
      "group": 1
    }, {
      "id": "Count",
      "group": 1
    }, {
      "id": "OldMan",
      "group": 1
    }, {
      "id": "Labarre",
      "group": 2
    }, {
      "id": "Valjean",
      "group": 2
    }, {
      "id": "Marguerite",
      "group": 3
    }, {
      "id": "Mme.deR",
      "group": 2
    }, {
      "id": "Isabeau",
      "group": 2
    }, {
      "id": "Gervais",
      "group": 2
    }, {
      "id": "Tholomyes",
      "group": 3
    }, {
      "id": "Listolier",
      "group": 3
    }, {
      "id": "Fameuil",
      "group": 3
    }, {
      "id": "Blacheville",
      "group": 3
    }, {
      "id": "Favourite",
      "group": 3
    }, {
      "id": "Dahlia",
      "group": 3
    }, {
      "id": "Zephine",
      "group": 3
    }, {
      "id": "Fantine",
      "group": 3
    }, {
      "id": "Mme.Thenardier",
      "group": 4
    }, {
      "id": "Thenardier",
      "group": 4
    }, {
      "id": "Cosette",
      "group": 5
    }, {
      "id": "Javert",
      "group": 4
    }, {
      "id": "Fauchelevent",
      "group": 0
    }, {
      "id": "Bamatabois",
      "group": 2
    }, {
      "id": "Perpetue",
      "group": 3
    }, {
      "id": "Simplice",
      "group": 2
    }, {
      "id": "Scaufflaire",
      "group": 2
    }, {
      "id": "Woman1",
      "group": 2
    }, {
      "id": "Judge",
      "group": 2
    }, {
      "id": "Champmathieu",
      "group": 2
    }, {
      "id": "Brevet",
      "group": 2
    }, {
      "id": "Chenildieu",
      "group": 2
    }, {
      "id": "Cochepaille",
      "group": 2
    }, {
      "id": "Pontmercy",
      "group": 4
    }, {
      "id": "Boulatruelle",
      "group": 6
    }, {
      "id": "Eponine",
      "group": 4
    }, {
      "id": "Anzelma",
      "group": 4
    }, {
      "id": "Woman2",
      "group": 5
    }, {
      "id": "MotherInnocent",
      "group": 0
    }, {
      "id": "Gribier",
      "group": 0
    }, {
      "id": "Jondrette",
      "group": 7
    }, {
      "id": "Mme.Burgon",
      "group": 7
    }, {
      "id": "Gavroche",
      "group": 8
    }, {
      "id": "Gillenormand",
      "group": 5
    }, {
      "id": "Magnon",
      "group": 5
    }, {
      "id": "Mlle.Gillenormand",
      "group": 5
    }, {
      "id": "Mme.Pontmercy",
      "group": 5
    }, {
      "id": "Mlle.Vaubois",
      "group": 5
    }, {
      "id": "Lt.Gillenormand",
      "group": 5
    }, {
      "id": "Marius",
      "group": 8
    }, {
      "id": "BaronessT",
      "group": 5
    }, {
      "id": "Mabeuf",
      "group": 8
    }, {
      "id": "Enjolras",
      "group": 8
    }, {
      "id": "Combeferre",
      "group": 8
    }, {
      "id": "Prouvaire",
      "group": 8
    }, {
      "id": "Feuilly",
      "group": 8
    }, {
      "id": "Courfeyrac",
      "group": 8
    }, {
      "id": "Bahorel",
      "group": 8
    }, {
      "id": "Bossuet",
      "group": 8
    }, {
      "id": "Joly",
      "group": 8
    }, {
      "id": "Grantaire",
      "group": 8
    }, {
      "id": "MotherPlutarch",
      "group": 9
    }, {
      "id": "Gueulemer",
      "group": 4
    }, {
      "id": "Babet",
      "group": 4
    }, {
      "id": "Claquesous",
      "group": 4
    }, {
      "id": "Montparnasse",
      "group": 4
    }, {
      "id": "Toussaint",
      "group": 5
    }, {
      "id": "Child1",
      "group": 10
    }, {
      "id": "Child2",
      "group": 10
    }, {
      "id": "Brujon",
      "group": 4
    }, {
      "id": "Mme.Hucheloup",
      "group": 8
    }],
    "links": [{
      "source": "Napoleon",
      "target": "Myriel",
      "value": 1
    }, {
      "source": "Mlle.Baptistine",
      "target": "Myriel",
      "value": 8
    }, {
      "source": "Mme.Magloire",
      "target": "Myriel",
      "value": 10
    }]
  }

  var link = g.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line");

  var node = g.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", 2.5)
    .on('click', clicked);

  node.append("title")
    .text(function(d) {
      return d.id;
    });

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

  simulation.force("link")
    .links(graph.links);

  function ticked() {
     // set up zoom transform:
     var xExtent = d3.extent(node.data(), function(d) { return d.x; });
     var yExtent = d3.extent(node.data(), function(d) { return d.y; });
            
     // get scales:    
     var xScale = width/(xExtent[1] - xExtent[0]);
     var yScale = height/(yExtent[1] - yExtent[0]);
     
     // get most restrictive scale
     var minScale = Math.min(xScale,yScale);
        
     if (minScale < 1) {
      var transform = d3.zoomIdentity.translate(width/2,height/2)
         .scale(minScale)
         .translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
       svg.call(zoom.transform, transform);   
     }

    link
      .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;
      });

    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr('r',20)
  }

  var active = d3.select(null);

  function clicked(d) {

    if (active.node() === this){
      active.classed("active", false);
      return reset();
    }
    
    active = d3.select(this).classed("active", true);

    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(8)
        .translate(-(+active.attr('cx')), -(+active.attr('cy')))
      );
  }

  function reset() {
    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(0, 0)
        .scale(1)
      );
  }

  function zoomed() {
    g.attr("transform", d3.event.transform);
  }

<script src="https://d3js.org/d3.v5.min.js"></script>

<svg width="960" height="600"></svg>

The issue with the above is that mouse events are essentially ignored during the simulation run - the tick events run quick enough to effectively over-write any changes due to mouse naviagation.

There are a few potential solutions:

  • Stop the autoscaling when the visualization cools down a enough that mouse navigation may be useful

  • Stop autoscaling when a user zoom is initiated

  • Don't enable user zoom until the force has cooled

I'll quickly implement the first one here, as it is likely the easiest. I'll also reduce the scale by a constant factor to give a bit of margin, so that when the autoscaling stops, nodes should remain in view. I've also changed the cursor during the time where mouse navigation won't result in visible changes (starts with wait, changes to pointer):

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  var zoom = d3.zoom()
    .scaleExtent([-8 / 2, 4])
    .on("zoom", zoomed);
    
    svg.call(zoom);

  var g = svg.append("g");

  var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

  var graph = {
    "nodes": [{
      "id": "Myriel",
      "group": 1
    }, {
      "id": "Napoleon",
      "group": 1
    }, {
      "id": "Mlle.Baptistine",
      "group": 1
    }, {
      "id": "Mme.Magloire",
      "group": 1
    }, {
      "id": "CountessdeLo",
      "group": 1
    }, {
      "id": "Geborand",
      "group": 1
    }, {
      "id": "Champtercier",
      "group": 1
    }, {
      "id": "Cravatte",
      "group": 1
    }, {
      "id": "Count",
      "group": 1
    }, {
      "id": "OldMan",
      "group": 1
    }, {
      "id": "Labarre",
      "group": 2
    }, {
      "id": "Valjean",
      "group": 2
    }, {
      "id": "Marguerite",
      "group": 3
    }, {
      "id": "Mme.deR",
      "group": 2
    }, {
      "id": "Isabeau",
      "group": 2
    }, {
      "id": "Gervais",
      "group": 2
    }, {
      "id": "Tholomyes",
      "group": 3
    }, {
      "id": "Listolier",
      "group": 3
    }, {
      "id": "Fameuil",
      "group": 3
    }, {
      "id": "Blacheville",
      "group": 3
    }, {
      "id": "Favourite",
      "group": 3
    }, {
      "id": "Dahlia",
      "group": 3
    }, {
      "id": "Zephine",
      "group": 3
    }, {
      "id": "Fantine",
      "group": 3
    }, {
      "id": "Mme.Thenardier",
      "group": 4
    }, {
      "id": "Thenardier",
      "group": 4
    }, {
      "id": "Cosette",
      "group": 5
    }, {
      "id": "Javert",
      "group": 4
    }, {
      "id": "Fauchelevent",
      "group": 0
    }, {
      "id": "Bamatabois",
      "group": 2
    }, {
      "id": "Perpetue",
      "group": 3
    }, {
      "id": "Simplice",
      "group": 2
    }, {
      "id": "Scaufflaire",
      "group": 2
    }, {
      "id": "Woman1",
      "group": 2
    }, {
      "id": "Judge",
      "group": 2
    }, {
      "id": "Champmathieu",
      "group": 2
    }, {
      "id": "Brevet",
      "group": 2
    }, {
      "id": "Chenildieu",
      "group": 2
    }, {
      "id": "Cochepaille",
      "group": 2
    }, {
      "id": "Pontmercy",
      "group": 4
    }, {
      "id": "Boulatruelle",
      "group": 6
    }, {
      "id": "Eponine",
      "group": 4
    }, {
      "id": "Anzelma",
      "group": 4
    }, {
      "id": "Woman2",
      "group": 5
    }, {
      "id": "MotherInnocent",
      "group": 0
    }, {
      "id": "Gribier",
      "group": 0
    }, {
      "id": "Jondrette",
      "group": 7
    }, {
      "id": "Mme.Burgon",
      "group": 7
    }, {
      "id": "Gavroche",
      "group": 8
    }, {
      "id": "Gillenormand",
      "group": 5
    }, {
      "id": "Magnon",
      "group": 5
    }, {
      "id": "Mlle.Gillenormand",
      "group": 5
    }, {
      "id": "Mme.Pontmercy",
      "group": 5
    }, {
      "id": "Mlle.Vaubois",
      "group": 5
    }, {
      "id": "Lt.Gillenormand",
      "group": 5
    }, {
      "id": "Marius",
      "group": 8
    }, {
      "id": "BaronessT",
      "group": 5
    }, {
      "id": "Mabeuf",
      "group": 8
    }, {
      "id": "Enjolras",
      "group": 8
    }, {
      "id": "Combeferre",
      "group": 8
    }, {
      "id": "Prouvaire",
      "group": 8
    }, {
      "id": "Feuilly",
      "group": 8
    }, {
      "id": "Courfeyrac",
      "group": 8
    }, {
      "id": "Bahorel",
      "group": 8
    }, {
      "id": "Bossuet",
      "group": 8
    }, {
      "id": "Joly",
      "group": 8
    }, {
      "id": "Grantaire",
      "group": 8
    }, {
      "id": "MotherPlutarch",
      "group": 9
    }, {
      "id": "Gueulemer",
      "group": 4
    }, {
      "id": "Babet",
      "group": 4
    }, {
      "id": "Claquesous",
      "group": 4
    }, {
      "id": "Montparnasse",
      "group": 4
    }, {
      "id": "Toussaint",
      "group": 5
    }, {
      "id": "Child1",
      "group": 10
    }, {
      "id": "Child2",
      "group": 10
    }, {
      "id": "Brujon",
      "group": 4
    }, {
      "id": "Mme.Hucheloup",
      "group": 8
    }],
    "links": [{
      "source": "Napoleon",
      "target": "Myriel",
      "value": 1
    }, {
      "source": "Mlle.Baptistine",
      "target": "Myriel",
      "value": 8
    }, {
      "source": "Mme.Magloire",
      "target": "Myriel",
      "value": 10
    }]
  }

  var link = g.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line");

  var node = g.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", 2.5)
    .on('click', clicked);

  node.append("title")
    .text(function(d) {
      return d.id;
    });

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

  simulation.force("link")
    .links(graph.links);
    
  var check = true;
  svg.attr("cursor","wait")

  function ticked() {
   
    if(this.alpha() > 0.04) {
      
    
       // set up zoom transform:
       var xExtent = d3.extent(node.data(), function(d) { return d.x; });
       var yExtent = d3.extent(node.data(), function(d) { return d.y; });

       // get scales:    
       var xScale = width/(xExtent[1] - xExtent[0]) * 0.75;
       var yScale = height/(yExtent[1] - yExtent[0]) * 0.75;

       // get most restrictive scale
       var minScale = Math.min(xScale,yScale);

       if (minScale < 1) {
        var transform = d3.zoomIdentity.translate(width/2,height/2)
           .scale(minScale)
           .translate(-(xExtent[0]+xExtent[1])/2,-(yExtent[0]+yExtent[1])/2)
         svg.call(zoom.transform, transform);   
       }
     }
     else {
      svg.attr("cursor","pointer")
      if(check) console.log("check");
      var check = false;
     }

    link
      .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;
      });

    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      })
      .attr('r',20)
  }

  var active = d3.select(null);

  function clicked(d) {

    if (active.node() === this){
      active.classed("active", false);
      return reset();
    }
    
    active = d3.select(this).classed("active", true);

    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(width / 2, height / 2)
        .scale(8)
        .translate(-(+active.attr('cx')), -(+active.attr('cy')))
      );
  }

  function reset() {
    svg.transition()
      .duration(750)
      .call(zoom.transform,
        d3.zoomIdentity
        .translate(0, 0)
        .scale(1)
      );
  }

  function zoomed() {
    g.attr("transform", d3.event.transform);
  }

<script src="https://d3js.org/d3.v5.min.js"></script>

<svg width="960" height="600"></svg>

The force could also only be rendered once a general idea of the ultimate bounds are known, avoiding any time where navigation is over-written by the autoscaling.

这篇关于根据边界动态设置初始d3缩放-V4的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆