score:4

Accepted answer

The simplest thing you can do is just assign a parent at random, then use that parent to position the nodes, and draw the links yourself.

var svg = d3
  .select("body")
  .append("svg")
  .attr("width", 600)
  .attr("height", 600)
  .append("g")
  .attr("transform", "translate(50,50)");

//tree data
var data = [
  { child: "Alice", parents: [] },
  { child: "Bob", parents: [] },
  { child: "Carol", parents: [] },
  { child: "Dave", parents: ["Alice", "Bob"] },
  { child: "Eve", parents: ["Alice", "Bob"] },
  { child: "Francis", parents: ["Bob", "Carol"] },
  { child: "Graham", parents: ["Carol"] },
  { child: "Hugh", parents: ["Eve", "Graham"] },
];

// Process the nodes, add a pseudo root node so we don't have
// multiple roots
data.forEach(function(d) {
  d.parentId = d.parents.length > 0 ? d.parents[0] : "root";
});
data.unshift({ child: "root", parentId: "" });

//to construct
var dataStructure = d3
  .stratify()
  .id(function (d) {
    return d.child;
  })
  .parentId(function (d) {
    return d.parentId;
  })(data);

//to define the size of the structure tree
var treeStructure = d3.tree().size([500, 300]);
var root = treeStructure(dataStructure);

var nodes = root.descendants()
  .filter(function(d) { return d.id !== "root"; });

// Custom way to get all links we need to draw
var links = [];
nodes.forEach(function(node) {
  node.data.parents.forEach(function(parentId) {
    var parentNode = nodes.find(function(d) { return d.id === parentId; });
    links.push({
      source: parentNode,
      target: node,
    });
  });
});

//to make the connections curves
var connections = svg.append("g").selectAll("path").data(links);
connections
  .enter()
  .append("path")
  .attr("d", function (d) {
    return (
      "M" +
      d.source.x +
      "," +
      d.source.y +
      " C " +
      d.source.x +
      "," +
      (d.source.y + d.target.y) / 2 +
      " " +
      d.target.x +
      "," +
      (d.source.y + d.target.y) / 2 +
      " " +
      d.target.x +
      "," +
      d.target.y
    );
  });

//creating the circles with data info
var circles = svg
  .append("g")
  .selectAll("circle")
  .data(nodes);

//placing the circles
circles
  .enter()
  .append("circle")
  .attr("cx", function (d) {
    return d.x;
  })
  .attr("cy", function (d) {
    return d.y;
  })
  .attr("r", 7)
  .append("text");

//names
var names = svg.append("g").selectAll("text").data(nodes);
names
  .enter()
  .append("text")
  .text(function (d) {
    return d.id;
  })
  .attr("x", function (d) {
    return d.x + 7;
  })
  .attr("y", function (d) {
    return d.y + 4;
  });
circle {
  fill: rgb(88, 147, 0);
}

path {
  fill: none;
  stroke: black;
}
<script src="https://d3js.org/d3.v6.min.js"></script>

However, you can see that this has some problems. We'd want the links to be as short as possible, so we should order the nodes by their parents - children with the same parent should be somewhere between both parents, and there shouldn't be any nodes between two parents if they have a partnership.

Possibly the simplest way to do that is by using a node-link diagram. By applying the right forces, you can push the nodes towards an optimal layout.

I force the links to be short, the tree to be centred in the middle of the diagram, the nodes to be at the correct levels, and each node to be 100 pixels away from any other nodes.

I run 100 ticks of the simulation and stop it before ever rendering anything, thus avoiding the movement and twitching that you normally see. As far as user experience goes, it looks like a normal tree-like structure.

I also use a custom attribute d.dy on the nodes as the "desired y-value". This value is the one that the nodes are attracted to for the simulation, but since it can deviate a little bit, it can look messy. By snapping the y-axis values like this, I avoided that and made it look more structured.

var levelRadius = 50;
var radius = 10;
var size = 500;
var margin = 50;

var svg = d3
  .select("body")
  .append("svg")
  .attr("width", size + 2 * margin)
  .attr("height", size + 2 * margin)
  .append("g")
  .attr("transform", "translate(" + [margin, margin] + ")");

//tree data
var data = [{
    child: "Alice",
    parents: []
  },
  {
    child: "Bob",
    parents: []
  },
  {
    child: "Carol",
    parents: []
  },
  {
    child: "Dave",
    parents: ["Alice", "Bob"]
  },
  {
    child: "Eve",
    parents: ["Alice", "Bob"]
  },
  {
    child: "Francis",
    parents: ["Bob", "Carol"]
  },
  {
    child: "Graham",
    parents: ["Carol"]
  },
  {
    child: "Hugh",
    parents: ["Eve", "Graham"]
  },
];

data.forEach(function(d, i) {
  d.level = d.parents.length ?
    data.find(function(p) {
      return p.child === d.parents[0];
    }).level + 1 :
    0;

  // Desired y level, otherwise try for sensible defaults
  d.dy = d.y = d.level * levelRadius * 2;
  d.x = size / 2 - i * levelRadius;
});

var nodes = data;

// Custom way to get all links we need to draw
var links = [];
nodes.forEach(function(node) {
  node.parents.forEach(function(parentId) {
    var parentNode = nodes.find(function(d) {
      return d.child === parentId;
    });
    links.push({
      id: parentNode.child + " - " + node.child,
      source: parentNode,
      target: node,
    });
  });
});

var simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).id(d => d.id))
  .force("collide", d3.forceCollide(levelRadius))
  .force("x", d3.forceX()
    .x(function(d) {
      return size / 2;
    })
    .strength(0.2)
  )
  .force("y", d3.forceY()
    .y(function(d) {
      return d.dy;
    })
    .strength(5)
  );
// Run the simulation once, even before rendering anything
simulation.tick(100)
  .stop();

//to make the connections curves
var link = svg.append("g").selectAll("path")
  .data(links)
  .enter()
  .append("path")
  .attr("d", function(d) {
    return (
      "M" +
      d.source.x +
      "," +
      d.source.dy +
      " C " +
      d.source.x +
      "," +
      (d.source.y + d.target.dy) / 2 +
      " " +
      d.target.x +
      "," +
      (d.source.y + d.target.dy) / 2 +
      " " +
      d.target.x +
      "," +
      d.target.dy
    );
  });

var node = svg
  .selectAll(".node")
  .data(nodes)
  .enter()
  .append("g")
  .attr("class", "node")
  .attr("transform", function(d) {
    // Use d.dy to snap the node to the level that we want it at
    return "translate(" + [d.x, d.dy] + ")";
  });

node.append("circle")
  .attr("r", radius);
node.append("text")
  .attr("dominant-baseline", "middle")
  .attr("dx", radius + 3)
  .text(function(d) {
    return d.child;
  });
circle {
  fill: rgb(88, 147, 0);
}

path {
  fill: none;
  stroke: black;
}
<script src="https://d3js.org/d3.v6.min.js"></script>


Related Query