Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 Force simulation in a grid

I was wondering how it would be possible to modify Mike Bostock's example of a multi-force layout in order to try and get the force layout to group nodes in a grid.

So let us imagine that we have the following csv:

Name, Category1, Category2
1,1,1
2,1,2
3,1,1
4,2,2
5,3,1
6,1,4
7,5,5
8,1,5
9,2,4
10,3,3
11,4,4
12,4,5
13,3,4
14,1,2
15,1,1
16,2,2
17,3,1
18,2,1
19,4,5
20,3,1

For his kind of data I would like to have all the possible values of Category 1 as columns and all the possible values of Category 2 as rows and would like my nodes to automatically group in the "proper" cell depending on their values for Category 1 and Category 2.

I am just getting started with D3 and don't really know where to start. The example I pointed to is useful, but it's hard to know what to modify as the code has close to no comments.

Any help would be appreciated.

like image 779
LBes Avatar asked May 09 '18 12:05

LBes


1 Answers

Forget that example: it uses D3 v3, which makes positioning the nodes way more complicated.

In D3 v4/v5 there are two convenient methods, forceX and forceY.

All you need to do is creating your scales, for instance using a point scale (the best choice here in my opinion):

var columnScale = d3.scalePoint()
  .domain(["1", "2", "3", "4", "5"])
  .range([min, max]);

var rowScale = d3.scalePoint()
  .domain(["1", "2", "3", "4", "5"])
  .range([min, max]);

And then use those scales in the simulation:

var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d) {
    return columnScale(d.Category1)
  }))
  .force("y", d3.forceY(function(d) {
    return rowScale(d.Category2)
  }))

Here is a basic demo with the data you shared (I'm using a colour scale to highlight the different positions on the grid):

var csv = `Name,Category1,Category2
1,1,1
2,1,2
3,1,1
4,2,2
5,3,1
6,1,4
7,5,5
8,1,5
9,2,4
10,3,3
11,4,4
12,4,5
13,3,4
14,1,2
15,1,1
16,2,2
17,3,1
18,2,1
19,4,5
20,3,1`;

var data = d3.csvParse(csv);

var w = 250,
  h = 250;

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var color = d3.scaleOrdinal(d3.schemeCategory10);

var columnScale = d3.scalePoint()
  .domain(dataRange(data, 'Category1')) // or ["1", "2", "3", "4", "5"]
  .range([30, w - 10])
  .padding(0.5);

var rowScale = d3.scalePoint()
  .domain(dataRange(data, 'Category2')) // or ["1", "2", "3", "4", "5"]
  .range([30, h - 10])
  .padding(0.5);

var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d) {
    return columnScale(d.Category1)
  }))
  .force("y", d3.forceY(function(d) {
    return rowScale(d.Category2)
  }))
  .force("collide", d3.forceCollide(6))

var nodes = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", 5)
  .attr("fill", function(d) {
    return color(d.Category1 + d.Category2)
  });

var xAxis = d3.axisTop(columnScale)(svg.append("g").attr("transform", "translate(0,30)"));

var yAxis = d3.axisLeft(rowScale)(svg.append("g").attr("transform", "translate(30,0)"));

simulation.on("tick", function() {
  nodes.attr("cx", function(d) {
      return d.x
    })
    .attr("cy", function(d) {
      return d.y
    })
});

function dataRange(records, field) {
  var min = d3.min(records.map(record => parseInt(record[field], 10)));
  var max = d3.max(records.map(record => parseInt(record[field], 10)));
  return d3.range(min, max + 1);
};
svg {
  background-color: floralwhite;
  border: 1px solid gray;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

PS: In both scales I'm using strings in the domain because d3.csv will load your data as strings, not numbers. Change that according to your needs.

like image 50
Gerardo Furtado Avatar answered Nov 11 '22 20:11

Gerardo Furtado