Search code examples
javascriptd3.jsforce-layout

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.


Solution

  • 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.