简体   繁体   中英

Zooming in a d3js force simulation on a canvas

I've set up a force directed graph with d3.js using svg, but eventually the graph became to big and it's having performance issues. I decided to try to do it in a canvas, because I read it renders stuff muck better and faster. But now I'm having an issue with zooming. I've implemented the zoom behavior correctly (i guess), but i can only zoom when the graph is at rest. Before the simulation find an equilibrium point zoom behavior does not work. Any idea why? Or any tips on what should i do?

var force = d3.forceSimulation()
            .force("link", d3.forceLink().id(function(d, i) { return i; }))
            .force("charge", d3.forceManyBody().strength( -5 ))
            .force("center", d3.forceCenter(width / 2, height / 2));

force.nodes(data.nodes)
    .on("tick", ticked)

force.force("link")
    .links(data.links);

function ticked(){
  context.clearRect(0, 0, width, height);

  // Draw the links
  data.links.forEach(function(d) {
      // Draw a line from source to target.
      context.beginPath();
      context.moveTo(d.source.x, d.source.y);
      context.lineTo(d.target.x, d.target.y);
      context.stroke();
  });
  // Draw the nodes 
  data.nodes.forEach(function(d, i) {
     // Draws a complete arc for each node.
     context.beginPath();
     context.arc(d.x, d.y, d.radius, 0, 2 * Math.PI, true);
     context.fill();
  });
};

// now the zooming part
  canvas.call( d3.zoom().scaleExtent([0.2, 10]).on("zoom", zoomed) )

  function zoomed(d) {
    context.save();
    context.clearRect(0, 0, width, height);
    context.translate(d3.event.transform.x, d3.event.transform.y);
    context.scale(d3.event.transform.k, d3.event.transform.k);

    // Draw the links ...
    data.links.forEach(function(d) {
        context.beginPath();
        context.moveTo(d.source.x, d.source.y);
        context.lineTo(d.target.x, d.target.y);
        context.stroke();
    });
    // Draw the nodes ...
    data.nodes.forEach(function(d, i) {
        context.beginPath();
        context.arc(d.x, d.y, d.radius, 0, 2 * Math.PI, true);
        context.fill();
    });
    context.restore();
  }

You tick function is not aware of any transforms created by the zoom. Best way to do this is to always apply a transform in the tick (which before any zooming is the identity transform ). This allows you to reuse the tick method to do all drawing.

var force = d3.forceSimulation()
  .force("link", d3.forceLink().id(function(d, i) {
    return d.id;
  }))
  .force("charge", d3.forceManyBody().strength(-5))
  .force("center", d3.forceCenter(width / 2, height / 2));

force.nodes(data.nodes)
  .on("tick", ticked);

force.force("link")
  .links(data.links)

var trans = d3.zoomIdentity; //<-- identity transform
function ticked() {
  context.save();
  context.clearRect(0, 0, width, height);
  context.translate(trans.x, trans.y); //<-- this always applies a transform
  context.scale(trans.k, trans.k);

  // Draw the links
  data.links.forEach(function(d) {
    // Draw a line from source to target.
    context.beginPath();
    context.moveTo(d.source.x, d.source.y);
    context.lineTo(d.target.x, d.target.y);
    context.stroke();
  });
  // Draw the nodes 
  data.nodes.forEach(function(d, i) {
    // Draws a complete arc for each node.
    context.beginPath();
    context.arc(d.x, d.y, 5, 0, 2 * Math.PI, true);
    context.fill();
  });

  context.restore();
};

// now the zooming part
canvas.call(d3.zoom().scaleExtent([0.2, 10]).on("zoom", zoomed))

function zoomed(d) {
  trans = d3.event.transform; //<-- set to current transform
  ticked(); //<-- use tick to redraw regardless of event
}

Full running code:

 <!DOCTYPE html> <html> <head> <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script> </head> <body> <canvas width="500" height="500"></canvas> <script> var width = 500, height = 500 canvas = document.querySelector("canvas"), context = canvas.getContext("2d"); canvas = d3.select(canvas); var data = { "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 }], "links": [{ "source": "Napoleon", "target": "Myriel", "value": 1 }, { "source": "Mlle.Baptistine", "target": "Myriel", "value": 8 }, { "source": "Mme.Magloire", "target": "Myriel", "value": 10 }, { "source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6 }, { "source": "CountessdeLo", "target": "Myriel", "value": 1 }, { "source": "Geborand", "target": "Myriel", "value": 1 }, { "source": "Champtercier", "target": "Myriel", "value": 1 }, { "source": "Cravatte", "target": "Myriel", "value": 1 }, { "source": "Count", "target": "Myriel", "value": 2 }, { "source": "OldMan", "target": "Myriel", "value": 1 }, { "source": "Valjean", "target": "Labarre", "value": 1 }, { "source": "Valjean", "target": "Mme.Magloire", "value": 3 }, { "source": "Valjean", "target": "Mlle.Baptistine", "value": 3 }, { "source": "Valjean", "target": "Myriel", "value": 5 }, { "source": "Marguerite", "target": "Valjean", "value": 1 }, { "source": "Mme.deR", "target": "Valjean", "value": 1 }, { "source": "Isabeau", "target": "Valjean", "value": 1 }, { "source": "Gervais", "target": "Valjean", "value": 1 }, { "source": "Listolier", "target": "Tholomyes", "value": 4 }, { "source": "Fameuil", "target": "Tholomyes", "value": 4 }, { "source": "Fameuil", "target": "Listolier", "value": 4 }, { "source": "Blacheville", "target": "Tholomyes", "value": 4 }, { "source": "Blacheville", "target": "Listolier", "value": 4 }, { "source": "Blacheville", "target": "Fameuil", "value": 4 }, { "source": "Favourite", "target": "Tholomyes", "value": 3 }, { "source": "Favourite", "target": "Listolier", "value": 3 }, { "source": "Favourite", "target": "Fameuil", "value": 3 }, { "source": "Favourite", "target": "Blacheville", "value": 4 }, { "source": "Dahlia", "target": "Tholomyes", "value": 3 }, { "source": "Dahlia", "target": "Listolier", "value": 3 }, { "source": "Dahlia", "target": "Fameuil", "value": 3 }, { "source": "Dahlia", "target": "Blacheville", "value": 3 }, { "source": "Dahlia", "target": "Favourite", "value": 5 }, { "source": "Zephine", "target": "Tholomyes", "value": 3 }, { "source": "Zephine", "target": "Listolier", "value": 3 }, { "source": "Zephine", "target": "Fameuil", "value": 3 }, { "source": "Zephine", "target": "Blacheville", "value": 3 }, { "source": "Zephine", "target": "Favourite", "value": 4 }, { "source": "Zephine", "target": "Dahlia", "value": 4 }, { "source": "Fantine", "target": "Tholomyes", "value": 3 }, { "source": "Fantine", "target": "Listolier", "value": 3 }, { "source": "Fantine", "target": "Fameuil", "value": 3 }, { "source": "Fantine", "target": "Blacheville", "value": 3 }, { "source": "Fantine", "target": "Favourite", "value": 4 }, { "source": "Fantine", "target": "Dahlia", "value": 4 }, { "source": "Fantine", "target": "Zephine", "value": 4 }, { "source": "Fantine", "target": "Marguerite", "value": 2 }] } var force = d3.forceSimulation() .force("link", d3.forceLink().id(function(d, i) { return d.id; })) .force("charge", d3.forceManyBody().strength(-5)) .force("center", d3.forceCenter(width / 2, height / 2)); force.nodes(data.nodes) .on("tick", ticked); force.force("link") .links(data.links) var trans = d3.zoomIdentity; function ticked() { context.save(); context.clearRect(0, 0, width, height); context.translate(trans.x, trans.y); context.scale(trans.k, trans.k); // Draw the links data.links.forEach(function(d) { // Draw a line from source to target. context.beginPath(); context.moveTo(d.source.x, d.source.y); context.lineTo(d.target.x, d.target.y); context.stroke(); }); // Draw the nodes data.nodes.forEach(function(d, i) { // Draws a complete arc for each node. context.beginPath(); context.arc(dx, dy, 5, 0, 2 * Math.PI, true); context.fill(); }); context.restore(); }; // now the zooming part canvas.call(d3.zoom().scaleExtent([0.2, 10]).on("zoom", zoomed)) function zoomed(d) { trans = d3.event.transform; ticked(); } </script> </body> </html>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM