4

I have a data set which has parent and child nodes. Parent nodes can link with other parent nodes and child nodes can link with their parent nodes. What I want to do is to put child nodes radially placed around parent nodes. The nodes look something like this

parent: [{
    id: 1,
    type: 'parent',
    x: ,
    y: ,
    vx: ,
    vy: ,
}, {
    id: 2
    x: ,
    y: ,
    vx: ,
    vy: ,
}]

child: [{
    id: 1,
    type: 'child',
    parent_node: {
        id: 1,
        x: ,
        y: ,
        vx: ,
        vy: ,
    },
    x: ,
    y: ,
    vx: ,
    vy: ,
}]

So I have the details of the parent node such as its x and y inside the child nodes.

I tried to assign x and y (as in the centre) dynamically but could not find a way to do it:

force('radial', d3.forceRadial()
    .radius(node => {
        if (node.type === 'child') {
            return 10
        }
        return 0
    })
    .x(node => {
        if (node.type === 'child') {
            return node.parent_node.x
        }
        return 0
    })
    .y(node => {
        if (node.type === 'child') {
            return node.parent_node.y
        }
        return 0
    })
)

Can something like this be done? So that the centre for each child node can be provided dynamically, based on their parent's position?

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
revant
  • 234
  • 3
  • 13

2 Answers2

2

Some time ago I created a version of d3.forceRadial that accepts a function for setting the x and y positions. You can see the pull request here, and the code is here

This pull request was not accepted yet, and given it's a quite old one and has no comment from Mike Bostock (D3 creator) I reckon it will never be. So, if you want, you can use this custom d3.forceRadial by copying the function in the link: https://pastebin.com/75j8vj3C

Then, just use your positioning functions with the customised force, like this:

simulation.force("radial", customRadial(radius, foo, bar))
//positioning functions--------------------------^----^

Or:

simulation.force("radial", customRadial()
    .radius(radius)
    .x(foo)
    .y(bar))

Where foo and bar are your functions for the x and y positions.

Here is a demo:

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

var data = d3.range(500).map(function(d) {
  return {
    node: "foo" + d,
    centerX: 100 + ~~((d / 100) % 20) * 100
  }
});

var simulation = d3.forceSimulation(data)
  .force("radius", customRadial().radius(40)
    .x(function(d) {
      return d.centerX
    })
    .y(100)
    .strength(1))
  .force("collide", d3.forceCollide().radius(3).strength(.8));

var nodes = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", 2.5)
  .style("fill", "teal")

simulation.on("tick", tick);

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


function customRadial(radius, x, y) {

  var constant = function(x) {
    return function() {
      return x;
    };
  };

  var nodes,
    strength = constant(0.1),
    strengths,
    radiuses,
    xs,
    ys;

  if (typeof radius !== "function") radius = constant(+radius);
  if (typeof x !== "function") x = constant(x == null ? 0 : +x);
  if (typeof y !== "function") y = constant(y == null ? 0 : +y);

  function force(alpha) {
    for (var i = 0, n = nodes.length; i < n; ++i) {
      var node = nodes[i],
        dx = node.x - xs[i] || 1e-6,
        dy = node.y - ys[i] || 1e-6,
        r = Math.sqrt(dx * dx + dy * dy),
        k = (radiuses[i] - r) * strengths[i] * alpha / r;
      node.vx += dx * k;
      node.vy += dy * k;
    }
  }

  function initialize() {
    if (!nodes) return;
    var i, n = nodes.length;
    strengths = new Array(n);
    radiuses = new Array(n);
    xs = new Array(n);
    ys = new Array(n);
    for (i = 0; i < n; ++i) {
      radiuses[i] = +radius(nodes[i], i, nodes);
      xs[i] = +x(nodes[i], i, nodes);
      ys[i] = +y(nodes[i], i, nodes);
      strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes);
    }
  }

  force.initialize = function(_) {
    nodes = _, initialize();
  };

  force.strength = function(_) {
    return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
  };

  force.radius = function(_) {
    return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
  };

  force.x = function(_) {
    return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x;
  };

  force.y = function(_) {
    return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y;
  };

  return force;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

If you don't want (or if you can't, for any reason) to use this custom code an easy workaround is creating several simulations, one for each group of elements, as I did in this data visualization.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
0

because d3.forceRadial().x().y() do not accept functions.

according to the source code (d3v5) they only accept numbers.

rioV8
  • 24,506
  • 3
  • 32
  • 49