动态设置基于边界的初始d3缩放 - V4

Nav*_*eon 6 javascript d3.js d3-force-directed

我在页面上显示了大量节点,由于节点放置,大多数时候圆圈离开屏幕的可见区域.

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

更新:

我为这个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);
  }
Run Code Online (Sandbox Code Playgroud)
<script src="https://d3js.org/d3.v5.min.js"></script>

<svg width="960" height="600"></svg>
Run Code Online (Sandbox Code Playgroud)

预期:

在此输入图像描述

And*_*eid 5

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

  1. 约束布局,或者在节点接近svg的边界时探索减小的力和速度。

  2. 在冷却时,随着力扩展到超过svg的范围,请更改缩放比例。

第一个通过限制视口中的节点来达到相同的效果。但是,节点的大小不会缩小,这可能导致相当多的混乱。有许多对堆栈溢出的问题和答案是应对这种做法(如本一个)。

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

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

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

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

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

接下来,我们得到x和y的比例尺:

var xScale = width/(xExtent[1]-xExtent[0]);
var yScale = height/(yExtent[1]-yExtent[0]);
Run Code Online (Sandbox Code Playgroud)

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

var minScale = Math.min(xScale,yScale);
Run Code Online (Sandbox Code Playgroud)

现在我们就像缩放到一个点时一样设置缩放标识,但是我们要居中的点是力布局的中间(我们可以使用刚计算出的范围来确定中间),而比例尺是我们刚刚确定的规模。但是,我们仅在满足某些条件的情况下应用更改-在下面的示例中,这将是节点超出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);
}
Run Code Online (Sandbox Code Playgroud)

下面是滴答函数中嵌入的这种方法的演示:

 var xExtent = d3.extent(node.data(), function(d) { return d.x; });
var yExtent = d3.extent(node.data(), function(d) { return d.y; });
Run Code Online (Sandbox Code Playgroud)
var xScale = width/(xExtent[1]-xExtent[0]);
var yScale = height/(yExtent[1]-yExtent[0]);
Run Code Online (Sandbox Code Playgroud)

上面的问题是,在模拟运行期间基本上忽略了鼠标事件-滴答事件运行得足够快,可以有效地覆盖由于鼠标导航引起的任何更改。

有一些潜在的解决方案:

  • 当可视化效果降到足以使鼠标导航有用时,停止自动缩放

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

  • 在力冷却之前不要启用用户缩放

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

var minScale = Math.min(xScale,yScale);
Run Code Online (Sandbox Code Playgroud)
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);
}
Run Code Online (Sandbox Code Playgroud)

一旦知道了极限范围的一般概念,也就可以施加力,避免在任何时候因自动缩放而覆盖导航的情况。