title: Upgrading bubble chart to v4+ with gravity/collide detection/effects
body: <p>I've got a <a href="http://jsfiddle.net/s50j2bqf/2/" rel="nofollow noreferrer">converted</a> d3v4 bubble chart, but there used to be so much more features like gravity/charge and collision detection for these kind of things in d3v3.  When the chart loads - I want to see a consistent/movement of the bubbles like watching frog spawn in a pond - they get close to each other through gravity - but have repellant/charge type properties. The bubbles also need to try and keep away from the edge.</p>
  <p><a href="https://jsfiddle.net/37r0z8ks/" rel="nofollow noreferrer">Here's</a> what I'm looking for with v3:</p>
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="true" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://d3js.org/d3.v3.min.js"&gt;&lt;/script&gt;
  
  
  
  &lt;div class="bubblechart" data-role="bubblechart" data-width="300" data-height="300" data=""&gt;
  &lt;/div&gt;
  
  
  &lt;script&gt;
    $(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 &gt; 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 &gt; 9) {
                    displacementText = -14;
                  }
  
                  return (d.x + displacementText);
                })
                .attr("y", function(d) {
                  var displacementText = 5;
                  if (d.count &gt; 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 &amp;&amp; (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 &lt; r) {
                    l = (l - r) / l * alpha;
                    d.x -= x *= l;
                    d.y -= y *= l;
                    quad.point.x += x;
                    quad.point.y += y;
                  }
                }
                return x1 &gt; nx2 || x2 &lt; nx1 || y1 &gt; ny2 || y2 &lt; 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);
      }
  
    });
  
  &lt;/script&gt;
  
  
  &lt;style&gt;
    .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%;
      }
    }
  
  &lt;/style&gt;</code></pre>
  </div>
  </div>
  </p>
  <p>This is what I have for v4, but I'm not able to reproduce the functionality of v3:</p>
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="false" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>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;
        });
    }
  }</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    background: #eeeeee;
  }
  
  .line {
    fill: none;
    stroke-width: 2px;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://d3js.org/d3.v4.min.js"&gt;&lt;/script&gt;
  &lt;h1&gt;BubbleChart I&lt;/h1&gt;
  &lt;div class="bubblechart" data-width="300" data-height="300" /&gt;</code></pre>
  </div>
  </div>
  </p>