Arash Howaida
Arash Howaida

Reputation: 2617

D3.js v5 modular swarm clusters (variable radius?)

I want to create a visual whereby a swarm contains one big circle and a bunch of satellite circles clinging around it. For a simple demonstration, I have prepared a small version of the data set; each item in the array should have one big circle and then however many smaller circles clinging to it:

var data = [
  {'wfoe':'wfoe1','products':d3.range(20)},
  {'wfoe':'wfoe2','products':d3.range(40)},
  {'wfoe':'wfoe3','products':d3.range(10)}
];

Here is a snippet of my progress:

var margins = {
  top: 100,
  bottom: 300,
  left: 100,
  right: 100
};

var height = 250;
var width = 900;

var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;

var svg = d3.select('body')
  .append('svg')
  .attr('width', totalWidth)
  .attr('height', totalHeight);

var graphGroup = svg.append('g')
  .attr('transform', "translate(" + margins.left + "," + margins.top + ")");



var data = [
  {'wfoe':'wfoe1','products':d3.range(20)},
  {'wfoe':'wfoe2','products':d3.range(40)},
  {'wfoe':'wfoe3','products':d3.range(10)}
];

var columns = 4;
var spacing = 250;
var vSpacing = 250;

var fmcG = graphGroup.selectAll('.fmc')
  .data(data)
  .enter()
  .append('g')
  .attr('class', 'fmc')
  .attr('id', (d, i) => 'fmc' + i)
  .attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
  });

var xScale = d3.scalePoint()
  .range([0, width])
  .domain([0, 100]);

var rScale = d3.scaleThreshold()
  .range([50,5])
  .domain([0,1]);

data.forEach(function(d, i) {
  d.x = (i % columns) * spacing;
  d.y = ~~((i / columns)) * vSpacing;
});


var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d,i) {
return (i % columns) * spacing;
  }).strength(0.1))
  .force("y", d3.forceY(function(d,i) {
return ~~((i / columns)) * vSpacing;
  }).strength(0.01))
  .force("collide", d3.forceCollide(function(d,i) { return rScale(i)}))
  .stop();

simulation.tick(75);

fmcG.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", function(d,i) {
return rScale(i)
  })
  .attr("cx", function(d) {
return d.x;
  })
  .attr("cy", function(d) {
return d.y;
  })
  .style('fill',"#003366");
<script src="https://d3js.org/d3.v5.min.js"></script>

I want to quickly point out that the big circle doesn't represent any data point (they are just going to house a name / logo). I just thought that including it in the simulation data would be the easiest way to introduce the needed force logic for the swarm circles. I thought that an elegant solution would be to use a threshold scale and let the first (i=0) datum always be the biggest circle. Here is what I mean:

var rScale = d3.scaleThreshold()
  .range([0, 1])
  .domain([50, 5]);

fmcG.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", function(d,i) {
    return rScale(i)
  })
  .attr("cx", function(d) {
    return d.x;
  })
  .attr("cy", function(d) {
    return d.y;
  })
  .style('fill',"#003366");

The result I mentioned above (three big circles with little circles all around them) was not achieved, and in fact very few circles were appended and the variable radius component didn't seem to be working as I thought it would. (also no errors displayed in the log).

Question

How can I iteratively create swarms that start with one big circle and append subsequent smaller circles around the initial big circle, as applicable to the sample data set?

Upvotes: 0

Views: 86

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

You could use a force simulation, like below, only this gives non-deterministic results. However, it's really good when you want to gradually add more nodes. In the below solution, I gave all related nodes a link to the center node, but didn't draw it. This made it possible for linked nodes to attract heavily.

On the other hand, you could also use a bubble chart if you want D3 to find the optimal packing solution for you, without the force working on them. Only downside is you'd have to call the packing function with all nodes every time, and the other nodes might shift because of the new one.

var margins = {
  top: 100,
  bottom: 300,
  left: 100,
  right: 100
};

var height = 250;
var width = 900;

var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;

var svg = d3.select('body')
  .append('svg')
  .attr('width', totalWidth)
  .attr('height', totalHeight);

var graphGroup = svg.append('g')
  .attr('transform', "translate(" + margins.left + "," + margins.top + ")");

var data = [{
    'wfoe': 'wfoe1',
    'products': d3.range(20).map(function(v) {
      return v.toString() + '_wfoe1';
    })
  },
  {
    'wfoe': 'wfoe2',
    'products': d3.range(40).map(function(v) {
      return v.toString() + '_wfoe2';
    })
  },
  {
    'wfoe': 'wfoe3',
    'products': d3.range(10).map(function(v) {
      return v.toString() + '_wfoe3';
    })
  }
];

var columns = 4;
var spacing = 250;
var vSpacing = 250;

function dataToNodesAndLinks(d) {
  // Create one giant array of points and
  // one link between each wfoe and each product
  var nodes = [{
    id: d.wfoe,
    center: true
  }];
  var links = [];

  d.products.forEach(function(p) {
    nodes.push({
      id: p,
      center: false
    });
    links.push({
      source: d.wfoe,
      target: p
    });
  });
  return {
    nodes: nodes,
    links: links
  };
}

var fmcG = graphGroup.selectAll('.fmc')
  .data(data.map(function(d, i) {
    return dataToNodesAndLinks(d, i);
  }))
  .enter()
  .append('g')
  .attr('class', 'fmc')
  .attr('id', (d, i) => 'fmc' + i)
  .attr('transform', (d, k) => {
    var horSpace = (k % columns) * spacing;
    var vertSpace = ~~((k / columns)) * vSpacing;
    return "translate(" + horSpace + "," + vertSpace + ")";
  });

var xScale = d3.scalePoint()
  .range([0, width])
  .domain([0, 100]);

var rScale = d3.scaleThreshold()
  .range([50, 5])
  .domain([0, 1]);

fmcG.selectAll("circle")
  .data(function(d) {
    return d.nodes;
  })
  .enter()
  .append("circle")
  .attr("id", function(d) {
    return d.id;
  })
  .attr("r", function(d, i) {
    return d.center ? rScale(i) * 5 : rScale(i);
  })
  .style('fill', function(d) { return d.center ? "darkred" : "#003366"; })

fmcG
  .each(function(d, i) {
    d3.forceSimulation(d.nodes)
      .force("collision", d3.forceCollide(function(d) {
        return d.center ? rScale(i) * 5 : rScale(i);
      }))
      .force("center", d3.forceCenter(0, 0))
      .force("link", d3
        .forceLink(d.links)
        .id(function(d) {
          return d.id;
        })
        .distance(0)
        .strength(2))
      .on('tick', ticked);
  });

function ticked() {
  fmcG.selectAll("circle")
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });
}
<script src="https://d3js.org/d3.v5.js"></script>

Upvotes: 2

Related Questions