Set initial d3 zoom based on boundary dynamically - V4
Asked Answered
K

2

6

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:

enter image description here

Keelby answered 21/4, 2018 at 21:12 Comment(0)
A
7

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.

Animated answered 24/4, 2018 at 3:35 Comment(0)
C
1

Check the size of a containing element once d3 has added all your data, then zoom accordingly. See this question about checking the size of containers in SVG.

One of the answers there actually has a d3 specific implementation:

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

Which I included here just because my d3 is a little rusty, but I believe you can do something like this with your existing g variable:

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

I am not familiar with d3's zoom, but in general you would do something like this:

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's zoom may add further complexity.

Cornelius answered 24/4, 2018 at 0:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.