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

时间:2018-04-21 21:12:32

标签: 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);
  }
<script src="https://d3js.org/d3.v5.min.js"></script>

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

预期:

enter image description here

2 个答案:

答案 0 :(得分:5)

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

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

  2. 在冷却时,当力展开超出svg的范围时,更改变焦。

  3. 通过绑定视口中的节点,第一个实现相同的效果。但是,节点的大小不会缩小,这可能会导致相当多的混乱。有关堆栈溢出的问题和答案有很多处理这种方法(例如one)。

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

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

    首先我们得到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; });
    

    我们也可以在这里容纳半径,我只是使用节点中心来回答这个问题

    接下来我们得到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": "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>

    上述问题是在模拟运行期间基本上忽略了鼠标事件 - 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": "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>

    只有在知道最终边界的一般概念时才能渲染力,从而避免任何时候自动缩放覆盖导航。

答案 1 :(得分:1)

d3添加完所有数据后,检查包含元素的大小,然后相应地缩放。请参阅this question about checking the size of containers in SVG

其中一个答案实际上有一个d3特定的实现:

 var height = d3.select('#myGroup').select('svg').node().getBBox().height;
 var width = d3.select('#myGroup').select('svg').node().getBBox().width;

我在这里包括的只是因为我的d3有点生疏,但我相信你可以使用现有的g变量做这样的事情:

g.node().getBBox().height
g.node().getBBox().width

我不熟悉d3的缩放功能,但一般来说你会做这样的事情:

let scaleRatioX = containerWidth / elementWidth
let scaleRatioY = containerHeight / elementHeight

elementWidth = elementWidth * scaleRatioX
elementHeight = elementHeight * scaleRatioY

// or, if you want to scale evenly
let scaleRatio = Math.min(scaleRatioX, scaleRatioY)

elementWidth = elementWidth * scaleRatio
elementHeight = elementHeight * scaleRatio

// then set the element size to the new values

d3的变焦可能会进一步增加复杂性。