使用重力/碰撞检测/效果将气泡图升级到 v4+

Posted

技术标签:

【中文标题】使用重力/碰撞检测/效果将气泡图升级到 v4+【英文标题】:Upgrading bubble chart to v4+ with gravity/collide detection/effects 【发布时间】:2021-04-09 19:36:13 【问题描述】:

我有一个converted d3v4 气泡图,但在 d3v3 中曾经有更多的功能,例如重力/电荷和碰撞检测。当图表加载时 - 我想看到气泡的一致/运动,例如观看青蛙在池塘中产卵 - 它们通过重力彼此靠近 - 但具有驱避剂/电荷类型的属性。气泡也需要尽量远离边缘。

Here's 我在寻找 v3 的内容:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>



<div class="bubblechart" data-role="bubblechart" data- data- data="">
</div>


<script>
  $(document).ready(function() 

    var $this = $('.bubblechart');
    //console.log("rendered div now engage d3", $this);
    // set el height and width etc.

    var w = $this.data("width");
    var h = $this.data("height");

    var data = [
      "label": "Chinese",
      "value": 20
    , 
      "label": "American",
      "value": 10
    , 
      "label": "Indian",
      "value": 50
    ];


    function colores_google(n) 
      var colores_g = ["#ff7276", "#4baad2", "#eaa2a5", "#e75763", "#a6a19e"];
      return colores_g[n % colores_g.length];
    


    var methods = 
      el: "",
      init: function(el, options) 

        var clone = options["data"];
        var that = this;

        //console.log("clone", clone);

        w = options["width"];
        h = options["height"];

        methods.el = el;

        methods.setup(clone, w, h);
        //methods.resizeChart(methods.el["selector"]);
      ,
      resizeChart: function(selector) 
        //alert(selector);
        var svg = $(selector + " .bubblechart");


        var aspect = svg.width() / svg.height();
        var targetWidth = svg.parent().parent().width();

        if (targetWidth != null) 
          svg.attr("width", targetWidth);
          svg.attr("height", Math.round(targetWidth / aspect));
        
      ,
      funnelData: function(data, width, height) 
        function getRandom(min, max) 
          return Math.floor(Math.random() * (max - min + 1)) + min;
        

        var max_amount = d3.max(data, function(d) 
          return parseInt(d.value)
        )
        var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])

        $.each(data, function(index, elem) 
          elem.radius = radius_scale(elem.value) * .8;
          elem.all = 'all';
          elem.x = getRandom(0, width);
          elem.y = getRandom(0, height);
        );

        return data;
      ,
      getMargin: function() 
        return 
          top: 30,
          right: 25,
          bottom: 50,
          left: 25
        ;
      ,
      setup: function(data, w, h) 

        methods.width = w;
        methods.height = h;

        methods.fill = d3.scale.ordinal()
          .range(["#d84b2a", "#beccae", "#7aa25c", "#008000"])

        var margin = methods.getMargin();

        var selector = methods.el;

        var padding = 50;

        /*
        var svg = d3.select(selector)
            .append("svg")
                .attr("class", "bubblechart")
                .attr("width", parseInt(w + margin.left + margin.right,10))
                .attr("height", parseInt(h + margin.top + margin.bottom,10))
                .attr('viewBox', "0 0 "+parseInt(w + margin.left + margin.right,10)+" "+parseInt(h + margin.top + margin.bottom,10))
                .attr('perserveAspectRatio', "xMinYMid")
            .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
        */

        var chart = d3.select(selector).append("svg:svg")
          .attr("class", "chart")
          .attr("width", w - (w / 5))
          .attr("height", h)
          .attr("preserveAspectRatio", "none")
          .attr("viewBox", "0 0 " + (w - (w / 5)) + " " + h + "")
          .append("svg:g")
          .attr("class", "bubblechart")
          .attr("transform", "translate(-10,0)");


        methods.force = d3.layout.force()
          .charge(100)
          .gravity(1200)
          .size([methods.width, methods.height])


        var bubbleholder = chart.append("g")
          .attr("class", "bubbleholder")

        var bubbles = bubbleholder.append("g")
          .attr("class", "bubbles")

        var labelbubble = bubbleholder.append("g")
          .attr("class", "labelbubble")



        //add legend
        var legendPaddingTop = 30;

        var legend = d3.select($this[0]).append("svg:svg")
          .attr("class", "legend")
          .attr("width", w / 5)
          .attr("height", h)
          .append("svg:g")
          .attr("class", "legendsection")
          .attr("transform", "translate(" + ((w / 4) + padding) + "," + legendPaddingTop + ")");


        var label_group = legend.append("svg:g")
          .attr("class", "label_group")
          .attr("transform", "translate(" + (-(w / 3) + 20) + "," + 0 + ")");

        var legend_group = legend.append("svg:g")
          .attr("class", "legend_group")
          .attr("transform", "translate(" + (-(w / 3) - 100) + "," + 0 + ")");



        //draw labels                       
        var labels = label_group.selectAll("text.labels")
          .data(data);

        var legendHeight = legendPaddingTop;
        var ySpace = 18;
        var labelPadding = 3;

        labels.enter().append("svg:text")
          .attr("class", "labels")
          .attr("dy", function(d, i) 
            legendHeight += ySpace;
            return (ySpace * i) + labelPadding;
          )
          .attr("text-anchor", function(d) 
            return "start";
          )
          .text(function(d) 
            return d.label;
          );

        labels.exit().remove();


        var legend = legend_group.selectAll("circle").data(data);

        legend.enter().append("svg:circle")
          .attr("cx", 100)
          .attr("cy", function(d, i) 
            return ySpace * i;
          )
          .attr("r", 7)
          .attr("width", 18)
          .attr("height", 18)
          .style("fill", function(d, i) 
            return colores_google(i);
          );

        legend.exit().remove();


        //reset legend height
        //console.log("optimum height for legend", legendHeight);
        $this.find('.legend').attr("height", legendHeight);


        //add data

        data = this.funnelData(data, methods.width, methods.height);

        var padding = 4;
        var maxRadius = d3.max(data, function(d) 
          return parseInt(d.radius)
        );


        var scale = (methods.width / 6) / 100;


        var nodes = bubbles.selectAll("circle")
          .data(data);


        // Enter
        nodes.enter()
          .append("circle")
          .attr("class", "node")
          .attr("cx", function(d) 
            return d.x;
          )
          .attr("cy", function(d) 
            return d.y;
          )
          .attr("r", 1)
          .style("fill", function(d, i) 
            return colores_google(i);
          )
          .call(methods.force.drag);

        // Update
        nodes
          .transition()
          .delay(300)
          .duration(1000)
          .attr("r", function(d) 
            return d.radius * scale;
          )

        // Exit
        nodes.exit()
          .transition()
          .duration(250)
          .attr("cx", function(d) 
            return d.x;
          )
          .attr("cy", function(d) 
            return d.y;
          )
          .attr("r", 1)
          .remove();



        var bubblelabels = labelbubble.selectAll("text")
          .data(data);


        // Enter
        bubblelabels.enter()
          .append("text")
          .attr("class", function(d) 
            var cls = "title";

            if (d.count > 9) 
              cls += " largetxt";
            

            return cls;
          )
          .text(function(d) 
            return d.count;
          )
          .attr("x", function(d) 
            return d.x;
          )
          .attr("y", function(d) 
            return (d.y) + 5;
          );

        // Update
        bubblelabels
          .transition()
          .delay(300)
          .duration(1000)

        // Exit
        bubblelabels.exit()
          .transition()
          .duration(250)
          .remove();



        draw('all');


        function draw(varname) 
          var foci = 
            "all": 
              name: "All",
              x: methods.width / 2,
              y: methods.height / 2
            
          ;
          methods.force.on("tick", tick(foci, varname, .55));
          methods.force.start();
        

        function tick(foci, varname, k) 
          return function(e) 
            data.forEach(function(o, i) 
              var f = foci[o[varname]];
              o.y += (f.y - o.y) * k * e.alpha;
              o.x += (f.x - o.x) * k * e.alpha;
            );
            nodes
              .each(collide(.1))
              .attr("cx", function(d) 
                return d.x;
              )
              .attr("cy", function(d) 
                return d.y;
              );


            bubblelabels
              .each(collide(.1))
              .attr("x", function(d) 
                var displacementText = -5;
                if (d.count > 9) 
                  displacementText = -14;
                

                return (d.x + displacementText);
              )
              .attr("y", function(d) 
                var displacementText = 5;
                if (d.count > 9) 
                  displacementText = 7;
                

                return (d.y + displacementText);
              );

          
        


        function collide(alpha) 
          var quadtree = d3.geom.quadtree(data);
          return function(d) 
            var r = d.radius + maxRadius + padding,
              nx1 = d.x - r,
              nx2 = d.x + r,
              ny1 = d.y - r,
              ny2 = d.y + r;
            quadtree.visit(function(quad, x1, y1, x2, y2) 
              if (quad.point && (quad.point !== d)) 
                var x = d.x - quad.point.x,
                  y = d.y - quad.point.y,
                  l = Math.sqrt(x * x + y * y),
                  r = d.radius + quad.point.radius + padding;
                if (l < r) 
                  l = (l - r) / l * alpha;
                  d.x -= x *= l;
                  d.y -= y *= l;
                  quad.point.x += x;
                  quad.point.y += y;
                
              
              return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
            );
          ;
        


      ,
      update: function(data) 
        methods.el = this;
        var selector = methods.el;

        //console.log("new data", data);

        methods.animateBubbles(selector, data);
      ,
      animateBubbles: function(selector, data) 



      ,
      oldData: ""
    ;


    var el = $this[0];

    var options = 
      data: data,
      width: $(el).data("width"),
      height: $(el).data("height")
    


    if (data) 
      methods.init(el, options);
    

  );

</script>


<style>
  .bubblechart 
    text-align: center;
    font-size: 12px;
  

  .bubblechart .legend .label_group text,
  .bubblechart .labelbubble text 
    fill: #ffffff;
  

  .bubblechart .labelbubble text 
    font-size: 15px;
  

  .bubblechart .labelbubble text.largetxt 
    font-size: 25px;
  

  @media screen and (max-width: 501px) 
    .bubblechart .chart 
      width: 100%;
      height: 100%;
    
  

</style>

这是我对 v4 的要求,但我无法重现 v3 的功能:

var $this = $('.bubblechart');

var data = [
  "label": "Chinese",
  "value": 20
, 
  "label": "American",
  "value": 10
, 
  "label": "Indian",
  "value": 50
];

var width = $this.data('width'),
  height = $this.data('height');

var color = d3.scaleOrdinal()
  .range(["#ff5200", "red", "green"]);

var margin = 
    top: 20,
    right: 15,
    bottom: 30,
    left: 20
  ,
  width = width - margin.left - margin.right,
  height = height - margin.top - margin.bottom;

var svg = d3.select($this[0])
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr('class', 'bubblechart')
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var bubbles = svg.append('g').attr('class', 'bubbles');

var force = d3.forceSimulation()
  .force("collide", d3.forceCollide(12))
  .force("center", d3.forceCenter(width / 2, height / 2))
  .nodes(data);

var bubbles = svg.append("g")
  .attr("class", "bubbles")

data = funnelData(data, width, height);

var padding = 4;
var maxRadius = d3.max(data, function(d) 
  return parseInt(d.radius)
);

var scale = (width / 6) / 100;

var nodes = bubbles.selectAll("circle")
  .data(data);

// Enter
nodes.enter()
  .append("circle")
  .attr("class", "node")
  .attr("cx", function(d) 
    return d.x;
  )
  .attr("cy", function(d) 
    return d.y;
  )
  .attr("r", 10)
  .style("fill", function(d, i) 
    return color(i);
  )
  .call(d3.drag());

// Update
nodes
  .transition()
  .delay(300)
  .duration(1000)
  .attr("r", function(d) 
    return d.radius * scale;
  )

// Exit
nodes.exit()
  .transition()
  .duration(250)
  .attr("cx", function(d) 
    return d.x;
  )
  .attr("cy", function(d) 
    return d.y;
  )
  .attr("r", 1)
  .remove();


draw('all');


function funnelData(data, width, height) 
  function getRandom(min, max) 
    return Math.floor(Math.random() * (max - min + 1)) + min;
  

  var max_amount = d3.max(data, function(d) 
    return parseInt(d.value)
  )
  var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])

  $.each(data, function(index, elem) 
    elem.radius = radius_scale(elem.value) * .8;
    elem.all = 'all';
    elem.x = width / 2;
    elem.y = height / 2;
  );

  return data;



function draw(varname) 
  var foci = 
    "all": 
      name: "All",
      x: width / 2,
      y: height / 2
    
  ;
  force.on("tick", tick(foci, varname, .55));


function tick(foci, varname, k) 
  return function(e) 
    bubbles.selectAll("circle")
      .attr("cx", function(d) 
        return d.x;
      )
      .attr("cy", function(d) 
        return d.y;
      );
  
body 
  background: #eeeeee;


.line 
  fill: none;
  stroke-width: 2px;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>BubbleChart I</h1>
<div class="bubblechart" data- data- />

【问题讨论】:

是否可以添加您想要的 v3 示例?至少,它可能只是力模拟本身的配置。我只看到 v4 代码代表您为实现一个尚未看到的目标所做的努力 - 特定 v3 强制布局的功能。否则,您的代码无法达到预期的确切原因尚不清楚。 jsfiddle.net/37r0z8ks 【参考方案1】:

过去在 d3v3 中有更多功能,例如重力/电荷和碰撞检测等功能

在强制布局方面,D3v4+ 比 d3v3 具有更好的功能。但是,具有讽刺意味的是,看看您共享的小提琴,您实际上并没有使用强制布局来实际放置节点。尝试将重力或电荷更改为完全不同的值 - 没有任何变化,您永远不会使用 force.nodes(data) 将节点传递给模拟。

也就是说,力模拟是提供一个alpha值,重复调用tick函数等等,但是节点和力模拟之间没有直接的交互。所有定位都是在刻度功能中手动完成的。可以用计时器代替力来达到同样的效果。

这表明你问错了问题,而不是

如何将布局升级到 v4

很有可能:

如何将自定义定位功能合并到 d3v4 的力布局中。

首先,让我们看看你的两个定位力:

fiddle中的碰撞检测内置于d3v4(d3.forceCollide)中,代码和你用的很相似。 多焦点点力有向图的完成方式与 d3v3 中类似。下面我使用 forceX 和 forceY 的单个焦点,这与您的小提琴相匹配。

这是一个简化的示例,说明您在此处使用起来更容易:

var width = 300,
height = 300,
svg = d3.select("body").append("svg")
   .attr("width", width)
   .attr("height", height)

var force = d3.layout.force()
        
var foci = x: 150, y:150
 
var data = funnelData(["label": "Chinese","value": 20, "label": "American","value": 10, "label": "Indian","value": 50],width,height)
    .map(function(d)  d.foci = foci; return d; )

force.start();
                
var node = svg.selectAll("circle")
    .data(data)
  .enter()
    .append('circle')
  .attr('r', 1)
  .attr('fill', function (d,i) 
      return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
   )
                
node.transition()
    .delay(300)
    .duration(1000)
    .attr("r", function(d)  return d.value * (width / 6) / 100; )
            
force.on("tick", function () 
    data.forEach(cluster);
    data.forEach(collide(0.1));
  
    node.attr("cx", function(d)  return d.x; )
      .attr("cy", function(d)  return d.y; )          
); 

function funnelData(data, width, height) 
   function getRandom(min, max) 
      return Math.floor(Math.random() * (max - min + 1)) + min;
   
   
   var max_amount = d3.max(data, function(d) 
      return parseInt(d.value)
   )
   var radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])

   data.forEach(function(elem, index) 
      elem.radius = radius_scale(elem.value) * .8;
      elem.all = 'all';
      elem.x = getRandom(0, width);
      elem.y = getRandom(0, height);
   );

   return data;

    
function cluster(d,i) 
    var f = d.foci;
    var k = 0.55;
    d.y += (f.y - d.y) * k * force.alpha()
    d.x += (f.x - d.x) * k * force.alpha()

    
var maxRadius = d3.max(data, function(d)  return d.radius; )
var padding = 4;
    
function collide(alpha) 
  var quadtree = d3.geom.quadtree(data);
     return function(d) 
          var r = d.radius + maxRadius + padding,
          nx1 = d.x - r,
          nx2 = d.x + r,
          ny1 = d.y - r,
          ny2 = d.y + r;
         quadtree.visit(function(quad, x1, y1, x2, y2) 
           if (quad.point && (quad.point !== d)) 
              var x = d.x - quad.point.x,
              y = d.y - quad.point.y,
              l = Math.sqrt(x * x + y * y),
              r = d.radius + quad.point.radius + padding;
             if (l < r) 
                l = (l - r) / l * alpha;
                d.x -= x *= l;
                d.y -= y *= l;
                quad.point.x += x;
                quad.point.y += y;
             
          
          return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
         );
        ;
    
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"&gt;&lt;/script&gt;

如果您删除修改数据和转换的刻度函数部分,您会看到节点保持完全静止。

我们可以很容易地将其更改为 d3v4+(嗯,d3-quad 需要第二次更新,否则,所有更改都是命名空间更改,例如:d3.scale.pow ⟶ d3.scalePow)。这让我们仍然使用您的定位功能并使用强制布局仅用于触发刻度、更改 alpha 等:

var width = 300,
height = 300,
svg = d3.select("body").append("svg")
   .attr("width", width)
   .attr("height", height)

var force = d3.forceSimulation();
        
var foci = x: 150, y:150
 
var data = funnelData(["label": "Chinese","value": 20, "label": "American","value": 10, "label": "Indian","value": 50],width,height)
    .map(function(d)  d.foci = foci; return d; )

force.nodes(data);
                
var node = svg.selectAll("circle")
    .data(data)
  .enter()
    .append('circle')
  .attr('r', 1)
  .attr('fill', function (d,i) 
      return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
   )
                
node.transition()
    .delay(300)
    .duration(1000)
    .attr("r", function(d)  return d.value * (width / 6) / 100; )
            
force.on("tick", function () 
    data.forEach(cluster);
    data.forEach(collide(0.1));
  
    node.attr("cx", function(d)  return d.x; )
      .attr("cy", function(d)  return d.y; )          
); 

function funnelData(data, width, height) 
   function getRandom(min, max) 
      return Math.floor(Math.random() * (max - min + 1)) + min;
   
   
   var max_amount = d3.max(data, function(d) 
      return parseInt(d.value)
   )
   var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])

   data.forEach(function(elem, index) 
      elem.radius = radius_scale(elem.value) * .8;
      elem.all = 'all';
      elem.x = getRandom(0, width);
      elem.y = getRandom(0, height);
   );

   return data;

    
function cluster(d,i) 
    var f = d.foci;
    var k = 0.55;
    d.y += (f.y - d.y) * k * force.alpha()
    d.x += (f.x - d.x) * k * force.alpha()

    
var maxRadius = d3.max(data, function(d)  return d.radius; )
var padding = 4;

function collide(alpha) 
  var quadtree = d3.quadtree().x(function(d)  return d.x; ).y(function(d)  return d.y; ).extent([[-1, -1], [width + 1, height + 1]]).addAll(data);
    
     return function(d) 
          var r = d.radius + maxRadius + padding,
          nx1 = d.x - r,
          nx2 = d.x + r,
          ny1 = d.y - r,
          ny2 = d.y + r;
         quadtree.visit(function(quad, x1, y1, x2, y2) 
           if (!quad.length && (quad.data !== d)) 
              var x = d.x - quad.data.x,
              y = d.y - quad.data.y,
              l = Math.sqrt(x * x + y * y),
              r = d.radius + quad.data.radius + padding;
              
              
             if (l < r) 
                l = (l - r) / l * alpha;
                    
                d.x -= x *= l;
                d.y -= y *= l;
                quad.data.x += x;
                quad.data.y += y;
             
          
          return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
         );
        ;
    
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"&gt;&lt;/script&gt;

但我们不要这样做,我们可以在 tick 函数中使用与之前相同的碰撞代码,但我们可以简化它。要使用 d3-forceSimulation 进行碰撞,我们可以使用d3.forceCollide()。我们想为其指定一个半径并修改碰撞力的强度(这是您的 sn-p 中的参数 alpha)。这可以按如下方式完成:

d3.forceCollide().radius(function(d)  return d.radius + padding; )
  .strength(0.1)

由于您只有一个焦点,我将使用 d3 的 forceX 和 forceY 将节点推向该点。相当于小提琴中的手动居中是:

  .force("x", d3.forceX().x(width/2)
           .strength(0.55))
  .force("y", d3.forceY().y(height/2)
           .strength(0.55))

总而言之:

var width = 300,
height = 300,
svg = d3.select("body").append("svg")
   .attr("width", width)
   .attr("height", height)
   
   
var data = funnelData(["label": "Chinese","value": 20, "label": "American","value": 10, "label": "Indian","value": 50],width,height)

var maxRadius = d3.max(data, function(d)  return d.radius; )
var padding = 4;


var force = d3.forceSimulation()
  .force("collide", d3.forceCollide()
           .radius(function(d)  return d.radius + padding + maxRadius; )
           .strength(0.1))
  .force("x", d3.forceX().x(width/2)
           .strength(0.55))
  .force("y", d3.forceY().y(height/2)
           .strength(0.55))
  .alpha(0.1) // same as v3.           

force.nodes(data);
                
var node = svg.selectAll("circle")
    .data(data)
  .enter()
    .append('circle')
  .attr('r', 1)
  .attr('fill', function (d,i) 
      return ["#ff7276", "#4baad2", "#eaa2a5", "#e75763","#a6a19e"][i%5];
   )
                
node.transition()
    .delay(300)
    .duration(1000)
    .attr("r", function(d)  return d.value * (width / 6) / 100; )
            
force.on("tick", function () 
    // Tinkered a bit here, did not dive into why a clearly identical solution was not immediately apparent.
    var alpha = this.alpha();
    this.force("collide").strength(0.2*alpha) 
      
    node.attr("cx", function(d)  return d.x; )
      .attr("cy", function(d)  return d.y; )          
); 

function funnelData(data, width, height) 
   function getRandom(min, max) 
      return Math.floor(Math.random() * (max - min + 1)) + min;
   
   
   var max_amount = d3.max(data, function(d) 
      return parseInt(d.value)
   )
   var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])

   data.forEach(function(elem, index) 
      elem.radius = radius_scale(elem.value) * .8;
      elem.all = 'all';
      elem.x = getRandom(0, width);
      elem.y = getRandom(0, height);
   );

   return data;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"&gt;&lt;/script&gt;

比较(时间差异只是我的滞后 - 我混淆了顺序,他们不遵循上述):

【讨论】:

tick函数有什么作用?你如何才能让气泡像青蛙产卵一样始终四处移动,并对它们产生这种重力/驱避电荷效应?如何避免气泡形成/漂浮在 svg 画布上? 勾号通常用于在模拟运行时手动定位节点,但也可用于应用自定义力(尽管您也可以将自定义力应用于force.force()),除了允许您在每个滴答声中手动执行某些操作外,tick 什么都不做。您可以使用刻度将节点约束到 SVG,eg(尽管您也可以使用 custom force。d3v3 力的电荷属性可以通过 d3-force (d3v4+) 的一部分的多体力来复制 如何确保气泡不会从 svg 画布上飘出?我总是在 svg 中呈现一半的气泡 小提琴中的碰撞半径相当大 - 它确保了相当多的空白空间,您可以只使用 d.radius+padding 而不是 d.radius+maxRadius+padding - 这可能会在页面上撞出东西,但是上面链接中的自定义力将有助于将它们保持在界限内(尽管您确实必须考虑圆的宽度),这可能是一个单独的问题(尽管我相信以前有人问过,但我目前找不到它)。

以上是关于使用重力/碰撞检测/效果将气泡图升级到 v4+的主要内容,如果未能解决你的问题,请参考以下文章

俄罗斯方块游戏开发系列教程4:形状碰撞检测(上)

iOS开发拓展篇—UIDynamic(重力行为+碰撞检测)

js 运动函数篇 (加速度运动弹性运动重力场运动(多方向+碰撞检测+重力加速度+能量损失运动)拖拽运动)层层深入

Unity之CharacterController 碰撞问题总结

UIDynamic(物理仿真)

万有引力与碰撞