javascript - 在 Canvas 上放大 d3js 力模拟

标签 javascript canvas d3.js

我已经使用 svg 通过 d3.js 设置了一个力定向图,但最终该图变得很大并且存在性能问题。我决定尝试在 Canvas 上进行此操作,因为我读到它可以更好更快地渲染东西。但现在我遇到了缩放问题。我已经正确实现了缩放行为(我猜),但我只能在图形静止时进行缩放。在模拟找到平衡点之前缩放行为不起作用。知道为什么吗?或者有什么建议我应该做什么?

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

最佳答案

您的tick函数不知道缩放创建的任何变换。最好的方法是始终在刻度中应用变换(在任何缩放之前是 identity transform )。这允许您重用tick方法来完成所有绘图。

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
}

完整运行代码:

<!DOCTYPE html>
<html>

<head>
  <script data-require="<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="73174033475d435d43" rel="noreferrer noopener nofollow">[email protected]</a>" 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(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;
      ticked();
    }
  </script>
</body>

</html>

关于javascript - 在 Canvas 上放大 d3js 力模拟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39807094/

相关文章:

javascript - 在 JSTL 上调用 javascript 函数 'form field'

html - 创建无限平行模式

javascript - D3 map 投影不显示 map

javascript - jquery Canvas 图像大小叠加

javascript - 在 Canvas 上打乱图像产生空白

javascript - D3 编码顺序

javascript - d3 GeoJSON geoCircle 椭圆等效

javascript - 两列中的三个 div - 等高

javascript - raphael.js 中的 z-index 分层

javascript - 如何使用 Angular 计算子项总数并在父项中显示