5

I am facing issues with the zoom function in D3 while using v4. It throws up an error saying that zoom.translate is not defined. I am mostly using the following code from this answer d3 focus on node on click, which worked perfectly for v3. However, as I was having issues with v3 as it has restrictions with data where the source and nodes are in the form of strings(instead of indices) D3 JSON file with source and index as strings rather than indices, I switched to v4.

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.links line {
  stroke: #999;
  stroke-opacity: 0.6;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}

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


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

var zoom = d3.zoom()
    .scaleExtent([1, 8])
    .on("zoom", zoomed);     

var color = d3.scaleOrdinal(d3.schemeCategory20);

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));

d3.json("graph.json", function(error, graph) {
  if (error) throw error;

  var link = svg.append("g")
      .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
      .attr("stroke-width", function(d) { return Math.sqrt(d.value); });

  var node = svg.append("g")
      .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
      .attr("r", 5)
      .attr("fill", function(d) { return color(d.group); })
      .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended))
            .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; });
  }
});

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

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

  var bbox = active.node().getBBox(),
      bounds = [[bbox.x, bbox.y],[bbox.x + bbox.width, bbox.y + bbox.height]];

  var dx = bounds[1][0] - bounds[0][0],
      dy = bounds[1][1] - bounds[0][1],
      x = (bounds[0][0] + bounds[1][0]) / 2,
      y = (bounds[0][1] + bounds[1][1]) / 2,
      scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
      translate = [width / 2 - scale * x, height / 2 - scale * y];

  svg.transition()
      .duration(750)
      .call(zoom.translate(translate).scale(scale).event);
} 

function reset() {
  active.classed("active", false);
  active = d3.select(null);

  svg.transition()
      .duration(750)
      .call(zoom.translate([0, 0]).scale(1).event);
} 

function zoomed() {
  console.log(d3.event)
  g.style("stroke-width", 1.5 / d3.event.scale + "px");
  g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}        

</script>

I changed d3.behaviour.zoom() to d3.zoom() and even changed

.call(zoom.translate(translate).scale(scale).event);

to

.call(d3.zoom().on("zoom", function () {
        svg.attr("transform", d3.event.transform)
}));

which throws up a strange error, error: unknown type: wheel

What would be the best way to go about overcoming this situation?

Community
  • 1
  • 1
VerletIntegrator
  • 159
  • 2
  • 10
  • In [my answer](http://stackoverflow.com/a/41917020/5768908) to your previous question, I didn't say that you should upgrade to v4. Actually, I showed you how to do it using v3. – Gerardo Furtado Jan 29 '17 at 05:55
  • Unfortunately, I do not have the data in an array, but in a JSON file. Can it still be done? – VerletIntegrator Jan 29 '17 at 06:13
  • Yes, it can. JSON is just syntax for storing data: your JSON probably contains an array. – Gerardo Furtado Jan 29 '17 at 06:22
  • It throws TypeError u.source.index is undefined: d3.json("graph.json", function(error, graph) { if (error) throw error; force.nodes(graph.nodes).links(graph.links).start(); link = link.data(graph.links).enter().append("line").attr("class", "links"); node = node.data(graph.nodes) .enter().append("circle").attr("class", "node").attr("r", 12).on("click", clicked); var nodeByName = d3.map(node, function(d) { return d.id; }); link.forEach(function(d) { d.source = nodeByName.get(d.source); d.target = nodeByName.get(d.target); }); }); – VerletIntegrator Jan 29 '17 at 06:43

1 Answers1

7

In d3 version 4 the correct way to do this is:

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')))
      );
  }

Where your zoom handler is:

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

Note, I simplified the transform calculation from my previous answer. The bounds calculations there were not really necessary.


Full code:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .links line {
    stroke: #aaa;
  }
  
  .nodes circle {
    pointer-events: all;
    stroke: none;
    stroke-width: 40px;
  }
  
  .active {
    fill: yellow;
  }
</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
  var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  var zoom = d3.zoom()
    .scaleExtent([1 / 2, 4])
    .on("zoom", zoomed);

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

  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>
Mark
  • 106,305
  • 20
  • 172
  • 230