草庐IT

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

coder 2024-12-18 原文

我有大量节点要显示在页面上,大多数情况下,由于节点放置,圆圈会超出屏幕的可见区域。

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

更新:

我为此添加了一个 fiddle 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>

预期:

最佳答案

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

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

  2. 随着力扩展到 svg 的边界之外,同时冷却,更改缩放。

第一个通过在视口(viewport)中限制节点来实现相同的效果。但是,节点的大小不会缩小,这可能会导致相当多的困惑。有许多关于处理这种方法的堆栈溢出问题和答案(例如 one)。

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

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

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

 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 事件运行得足够快,可以有效地覆盖由于鼠标导航引起的任何更改。

有一些潜在的解决方案:

  • 当可视化降温到鼠标导航可能有用时停止自动缩放

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

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

我将在这里快速实现第一个,因为它可能是最简单的。我还将按常数因子减小比例以留出一些余量,以便在自动缩放停止时,节点应保持在 View 中。我还在鼠标导航不会导致可见更改的时间更改了光标(从等待开始,更改为指针):

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>

力也只能在知道最终边界的一般概念后才能呈现,避免任何时候导航被自动缩放覆盖。

关于javascript - 基于边界动态设置初始 d3 缩放 - V4,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49960250/

有关javascript - 基于边界动态设置初始 d3 缩放 - V4的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  3. ruby-on-rails - 未初始化的常量 Psych::Syck (NameError) - 2

    在我的gem中,我需要yaml并且在我的本地计算机上运行良好。但是在将我的gem推送到ruby​​gems.org之后,当我尝试使用我的gem时,我收到一条错误消息=>"uninitializedconstantPsych::Syck(NameError)"谁能帮我解决这个问题?附言RubyVersion=>ruby1.9.2,GemVersion=>1.6.2,Bundlerversion=>1.0.15 最佳答案 经过几个小时的研究,我发现=>“YAML使用未维护的Syck库,而Psych使用现代的LibYAML”因此,为了解决

  4. ruby-on-rails - 如何使用 instance_variable_set 正确设置实例变量? - 2

    我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击

  5. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

  6. ruby-on-rails - date_field_tag,如何设置默认日期? [ rails 上的 ruby ] - 2

    我想设置一个默认日期,例如实际日期,我该如何设置?还有如何在组合框中设置默认值顺便问一下,date_field_tag和date_field之间有什么区别? 最佳答案 试试这个:将默认日期作为第二个参数传递。youcorrectlysetthedefaultvalueofcomboboxasshowninyourquestion. 关于ruby-on-rails-date_field_tag,如何设置默认日期?[rails上的ruby],我们在StackOverflow上找到一个类似的问

  7. ruby-on-rails - ActionController::RoutingError: 未初始化常量 Api::V1::ApiController - 2

    我有用于控制用户任务的Rails5API项目,我有以下错误,但并非总是针对相同的Controller和路由。ActionController::RoutingError:uninitializedconstantApi::V1::ApiController我向您描述了一些我的项目,以更详细地解释错误。应用结构路线scopemodule:'api'donamespace:v1do#=>Loginroutesscopemodule:'login'domatch'login',to:'sessions#login',as:'login',via::postend#=>Teamroutessc

  8. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  9. ruby - 这两个 Ruby 类初始化定义有什么区别? - 2

    我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是

  10. ruby-on-rails - 有没有办法为 CarrierWave/Fog 设置上传进度指示器? - 2

    我在Rails应用程序中使用CarrierWave/Fog将视频上传到AmazonS3。有没有办法判断上传的进度,让我可以显示上传进度如何? 最佳答案 CarrierWave和Fog本身没有这种功能;你需要一个前端uploader来显示进度。当我不得不解决这个问题时,我使用了jQueryfileupload因为我的堆栈中已经有jQuery。甚至还有apostonCarrierWaveintegration因此您只需按照那里的说明操作即可获得适用于您的应用的进度条。 关于ruby-on-r

随机推荐