d3.js relazione multipla visual / linkHorizontal () / tangled tree

Oct 19 2020

Sto cercando di imitare un elemento visivo che raffigura più relazioni per periodo di tempo, come questo (periodo di tempo = generazione):

Tuttavia, finora i miei sforzi non sono andati a buon fine; Ricevo ancora output vuoto nel browser. Dati e codice hardcoded nello snippet:

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

var height = 600;
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 levels = [
    [{id: 'Chaos'}],
    [
      {id: 'Gaea', parents: ['Chaos']},
      {id: 'Uranus'}
    ],
    [
      {id: 'Oceanus', parents: ['Gaea', 'Uranus']},
      {id: 'Thethys', parents: ['Gaea', 'Uranus']},
      {id: 'Pontus'},
      {id: 'Rhea', parents: ['Gaea', 'Uranus']},
      {id: 'Cronus', parents: ['Gaea', 'Uranus']},
      {id: 'Coeus', parents: ['Gaea', 'Uranus']},
      {id: 'Phoebe', parents: ['Gaea', 'Uranus']},
      {id: 'Crius', parents: ['Gaea', 'Uranus']},
      {id: 'Hyperion', parents: ['Gaea', 'Uranus']},
      {id: 'Iapetus', parents: ['Gaea', 'Uranus']},
      {id: 'Thea', parents: ['Gaea', 'Uranus']},
      {id: 'Themis', parents: ['Gaea', 'Uranus']},
      {id: 'Mnemosyne', parents: ['Gaea', 'Uranus']}
    ],
    [
      {id: 'Doris', parents: ['Oceanus', 'Thethys']},
      {id: 'Neures', parents: ['Pontus', 'Gaea']},
      {id: 'Dionne'},
      {id: 'Demeter', parents: ['Rhea', 'Cronus']},
      {id: 'Hades', parents: ['Rhea', 'Cronus']},
      {id: 'Hera', parents: ['Rhea', 'Cronus']},
      {id: 'Alcmene'},
      {id: 'Zeus', parents: ['Rhea', 'Cronus']},
      {id: 'Eris'},
      {id: 'Leto', parents: ['Coeus', 'Phoebe']},
      {id: 'Amphitrite'},
      {id: 'Medusa'},
      {id: 'Poseidon', parents: ['Rhea', 'Cronus']},
      {id: 'Hestia', parents: ['Rhea', 'Cronus']}
    ],
    [
      {id: 'Thetis', parents: ['Doris', 'Neures']},
      {id: 'Peleus'},
      {id: 'Anchises'},
      {id: 'Adonis'},
      {id: 'Aphrodite', parents: ['Zeus', 'Dionne']},
      {id: 'Persephone', parents: ['Zeus', 'Demeter']},
      {id: 'Ares', parents: ['Zeus', 'Hera']},
      {id: 'Hephaestus', parents: ['Zeus', 'Hera']},
      {id: 'Hebe', parents: ['Zeus', 'Hera']},
      {id: 'Hercules', parents: ['Zeus', 'Alcmene']},
      {id: 'Megara'},
      {id: 'Deianira'},
      {id: 'Eileithya', parents: ['Zeus', 'Hera']},
      {id: 'Ate', parents: ['Zeus', 'Eris']},
      {id: 'Leda'},
      {id: 'Athena', parents: ['Zeus']},
      {id: 'Apollo', parents: ['Zeus', 'Leto']},
      {id: 'Artemis', parents: ['Zeus', 'Leto']},
      {id: 'Triton', parents: ['Poseidon', 'Amphitrite']},
      {id: 'Pegasus', parents: ['Poseidon', 'Medusa']},
      {id: 'Orion', parents: ['Poseidon']},
      {id: 'Polyphemus', parents: ['Poseidon']}
    ],
    [
      {id: 'Deidamia'},
      {id: 'Achilles', parents: ['Peleus', 'Thetis']},
      {id: 'Creusa'},
      {id: 'Aeneas', parents: ['Anchises', 'Aphrodite']},
      {id: 'Lavinia'},
      {id: 'Eros', parents: ['Hephaestus', 'Aphrodite']},
      {id: 'Helen', parents: ['Leda', 'Zeus']},
      {id: 'Menelaus'},
      {id: 'Polydueces', parents: ['Leda', 'Zeus']}
    ],
    [
      {id: 'Andromache'},
      {id: 'Neoptolemus', parents: ['Deidamia', 'Achilles']},
      {id: 'Aeneas(2)', parents: ['Creusa', 'Aeneas']},
      {id: 'Pompilius', parents: ['Creusa', 'Aeneas']},
      {id: 'Iulus', parents: ['Lavinia', 'Aeneas']},
      {id: 'Hermione', parents: ['Helen', 'Menelaus']}
    ]
  ]

  // precompute level depth
  levels.forEach((l,i) => l.forEach(n => n.level = i))

  var nodes = levels.reduce( ((a,x) => a.concat(x)), [] )
  var nodes_index = {}
  nodes.forEach(d => nodes_index[d.id] = d)

  // objectification
  nodes.forEach(d => {
    d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
  })

  // precompute bundles
  levels.forEach((l, i) => {
    var index = {}
    l.forEach(n => {
      if(n.parents.length == 0) {
        return
      }

      var id = n.parents.map(d => d.id).sort().join('--')
      if (id in index) {
        index[id].parents = index[id].parents.concat(n.parents)
      }
      else {
        index[id] = {id: id, parents: n.parents.slice(), level: i}
      }
      n.bundle = index[id]
    })
    l.bundles = Object.keys(index).map(k => index[k])
    l.bundles.forEach((b, i) => b.i = i)
  })

  var links = []
  nodes.forEach(d => {
    d.parents.forEach(p => links.push({source: d, bundle: d.bundle, target: p}))
  })

  var bundles = levels.reduce( ((a,x) => a.concat(x.bundles)), [] )

  // reverse pointer from parent to bundles
  bundles.forEach(b => b.parents.forEach(p => {
    if(p.bundles_index === undefined) {
      p.bundles_index = {}
    }
    if(!(b.id in p.bundles_index)) {
      p.bundles_index[b.id] = []
    }
    p.bundles_index[b.id].push(b)
  }))

  nodes.forEach(n => {
    if(n.bundles_index !== undefined) {
      n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
    }
    else {
      n.bundles_index = {}
      n.bundles = []
    }
    n.bundles.forEach((b, i) => b.i = i)
  })

  links.forEach(l => {
    if(l.bundle.links === undefined) {
      l.bundle.links = []
    }
    l.bundle.links.push(l)
  })

  // layout
  const padding = 8
  const node_height = 22
  const node_width = 70
  const bundle_width = 14
  const level_y_padding = 16
  const metro_d = 4
  const c = 16
  const min_family_height = 16

  nodes.forEach(n => n.height = (Math.max(1, n.bundles.length)-1)*metro_d)

  var x_offset = padding
  var y_offset = padding
  levels.forEach(l => {
    x_offset += l.bundles.length*bundle_width
    y_offset += level_y_padding
    l.forEach((n, i) => {
      n.x = n.level*node_width + x_offset
      n.y = node_height + y_offset + n.height/2

      y_offset += node_height + n.height
    })
  })

  var i = 0
  levels.forEach(l => {
    l.bundles.forEach(b => {
      b.x = b.parents[0].x + node_width + (l.bundles.length-1-b.i)*bundle_width
      b.y = i*node_height
    })
    i += l.length
  })

  links.forEach(l => {
    l.xt = l.target.x
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i*metro_d - l.target.bundles.length*metro_d/2 + metro_d/2
    l.xb = l.bundle.x
    l.xs = l.source.x
    l.ys = l.source.y
  })

  // compress vertical space
  var y_negative_offset = 0
  levels.forEach(l => {
    y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys-c)-(link.yt+c))) || 0
    l.forEach(n => n.y -= y_negative_offset)
  })

  // very ugly, I know
  links.forEach(l => {
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i*metro_d - l.target.bundles.length*metro_d/2 + metro_d/2
    l.ys = l.source.y
    l.c1 = l.source.level-l.target.level > 1 ? node_width+c : c
    l.c2 = c
  })

  const cluster = d3.cluster()
    .size([width, height]);

  const root = d3.hierarchy(links);
  cluster(root);

  var nodeG = svg.selectAll('.node')
      .data(root.links())
      .attr('class','node')
      .enter()
      .append('g');

      nodeG.append("path")
        .attr("class", "link")
        .attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))
        .style('stroke-width', '3px');
<script src="https://d3js.org/d3.v5.min.js"></script>

Per quanto ne so, tutti i pezzi sono a posto. Ho i miei dati levelse poi ho combattuto le coordinate gerarchiche necessarie usando:

  var links = []
  nodes.forEach(d => {
    d.parents.forEach(p => links.push({source: d, bundle: d.bundle, target: p}))
  })

e

  const cluster = d3.cluster()
    .size([width, height]);

  const root = d3.hierarchy(links);
  cluster(root);

  var nodeG = svg.selectAll('.node')
      .data(root.links())
      .attr('class','node')
      .enter()
      .append('g');

Da qui, sono andato con la d3.linkHorizontal()mia funzione di collegamento:

      nodeG.append("path")
        .attr("class", "link")
        .attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))
        .style('stroke-width', '3px');

Concettualmente, non vedo come l'inclusione di più relazioni per nodo cambi le cose. E, in assenza di errori nel registro della console, non sono sicuro di come risolvere ulteriormente i problemi.

Domanda

Cosa impedisce al mio elemento visivo di essere visualizzato come desiderato (nell'immagine sopra)? Vorrei una replica esatta, se possibile.

modificare

Ecco la visuale su osservabile se aiuta, ma non può essere vista come una visuale autonoma.

https://observablehq.com/@nitaku/tangled-tree-visualization-ii?collection=@nitaku/tangled-trees

Risposte

1 PeterDarmis Oct 28 2020 at 12:08

Niente viene aggiunto svgall'elemento tranne graphGroup. Apparentemente root.links()restituisce un array vuoto e non viene aggiunto nulla nel file svg. Questo è anche il motivo per cui non ricevi alcun errore.

Creando questo array e iterando su di esso, viene implementata la forma di base che vuoi ottenere nel tuo albero se cambi anche:

.attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))

con:

 .attr("d", d3.linkHorizontal()
          .source(d => [d.xs,d.ys] )
          .target(d => [d.xt,d.yt]))

La forma di base dell'albero che desideri implementare può essere vista nel frammento di seguito. Prova a vedere se questo esempio potrebbe aiutarti a modellare il tuo albero come desiderato.

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

var height = 600;
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 levels = [
  [{
    id: 'Chaos'
  }],
  [{
      id: 'Gaea',
      parents: ['Chaos']
    },
    {
      id: 'Uranus'
    }
  ],
  [{
      id: 'Oceanus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Thethys',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Pontus'
    },
    {
      id: 'Rhea',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Cronus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Coeus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Phoebe',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Crius',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Hyperion',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Iapetus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Thea',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Themis',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Mnemosyne',
      parents: ['Gaea', 'Uranus']
    }
  ],
  [{
      id: 'Doris',
      parents: ['Oceanus', 'Thethys']
    },
    {
      id: 'Neures',
      parents: ['Pontus', 'Gaea']
    },
    {
      id: 'Dionne'
    },
    {
      id: 'Demeter',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hades',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hera',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Alcmene'
    },
    {
      id: 'Zeus',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Eris'
    },
    {
      id: 'Leto',
      parents: ['Coeus', 'Phoebe']
    },
    {
      id: 'Amphitrite'
    },
    {
      id: 'Medusa'
    },
    {
      id: 'Poseidon',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hestia',
      parents: ['Rhea', 'Cronus']
    }
  ],
  [{
      id: 'Thetis',
      parents: ['Doris', 'Neures']
    },
    {
      id: 'Peleus'
    },
    {
      id: 'Anchises'
    },
    {
      id: 'Adonis'
    },
    {
      id: 'Aphrodite',
      parents: ['Zeus', 'Dionne']
    },
    {
      id: 'Persephone',
      parents: ['Zeus', 'Demeter']
    },
    {
      id: 'Ares',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hephaestus',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hebe',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hercules',
      parents: ['Zeus', 'Alcmene']
    },
    {
      id: 'Megara'
    },
    {
      id: 'Deianira'
    },
    {
      id: 'Eileithya',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Ate',
      parents: ['Zeus', 'Eris']
    },
    {
      id: 'Leda'
    },
    {
      id: 'Athena',
      parents: ['Zeus']
    },
    {
      id: 'Apollo',
      parents: ['Zeus', 'Leto']
    },
    {
      id: 'Artemis',
      parents: ['Zeus', 'Leto']
    },
    {
      id: 'Triton',
      parents: ['Poseidon', 'Amphitrite']
    },
    {
      id: 'Pegasus',
      parents: ['Poseidon', 'Medusa']
    },
    {
      id: 'Orion',
      parents: ['Poseidon']
    },
    {
      id: 'Polyphemus',
      parents: ['Poseidon']
    }
  ],
  [{
      id: 'Deidamia'
    },
    {
      id: 'Achilles',
      parents: ['Peleus', 'Thetis']
    },
    {
      id: 'Creusa'
    },
    {
      id: 'Aeneas',
      parents: ['Anchises', 'Aphrodite']
    },
    {
      id: 'Lavinia'
    },
    {
      id: 'Eros',
      parents: ['Hephaestus', 'Aphrodite']
    },
    {
      id: 'Helen',
      parents: ['Leda', 'Zeus']
    },
    {
      id: 'Menelaus'
    },
    {
      id: 'Polydueces',
      parents: ['Leda', 'Zeus']
    }
  ],
  [{
      id: 'Andromache'
    },
    {
      id: 'Neoptolemus',
      parents: ['Deidamia', 'Achilles']
    },
    {
      id: 'Aeneas(2)',
      parents: ['Creusa', 'Aeneas']
    },
    {
      id: 'Pompilius',
      parents: ['Creusa', 'Aeneas']
    },
    {
      id: 'Iulus',
      parents: ['Lavinia', 'Aeneas']
    },
    {
      id: 'Hermione',
      parents: ['Helen', 'Menelaus']
    }
  ]
]

// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));

var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);

// objectification
nodes.forEach(d => {
  d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})

// precompute bundles
levels.forEach((l, i) => {
  var index = {}
  l.forEach(n => {
    if (n.parents.length == 0) {
      return
    }

    var id = n.parents.map(d => d.id).sort().join('--')
    if (id in index) {
      index[id].parents = index[id].parents.concat(n.parents)
    } else {
      index[id] = {
        id: id,
        parents: n.parents.slice(),
        level: i
      }
    }
    n.bundle = index[id]
  })
  l.bundles = Object.keys(index).map(k => index[k])
  l.bundles.forEach((b, i) => b.i = i)
})

var links = []
nodes.forEach(d => {
  d.parents.forEach(p => links.push({
    source: d,
    bundle: d.bundle,
    target: p
  }))
})

var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])

// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
  if (p.bundles_index === undefined) {
    p.bundles_index = {}
  }
  if (!(b.id in p.bundles_index)) {
    p.bundles_index[b.id] = []
  }
  p.bundles_index[b.id].push(b)
}))

nodes.forEach(n => {
  if (n.bundles_index !== undefined) {
    n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
  } else {
    n.bundles_index = {}
    n.bundles = []
  }
  n.bundles.forEach((b, i) => b.i = i)
})

links.forEach(l => {
  if (l.bundle.links === undefined) {
    l.bundle.links = []
  }
  l.bundle.links.push(l)
})

// layout
const padding = 8
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16

nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)

var x_offset = padding
var y_offset = padding
levels.forEach(l => {
  x_offset += l.bundles.length * bundle_width
  y_offset += level_y_padding
  l.forEach((n, i) => {
    n.x = n.level * node_width + x_offset
    n.y = node_height + y_offset + n.height / 2

    y_offset += node_height + n.height
  })
})

var i = 0
levels.forEach(l => {
  l.bundles.forEach(b => {
    b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
    b.y = i * node_height
  })
  i += l.length
})

links.forEach(l => {
  l.xt = l.target.x
  l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
  l.xb = l.bundle.x
  l.xs = l.source.x
  l.ys = l.source.y
})

// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
  y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
  l.forEach(n => n.y -= y_negative_offset)
})

// very ugly, I know
links.forEach(l => {
  l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
  l.ys = l.source.y
  l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
  l.c2 = c
})

const cluster = d3.cluster()
  .size([width, height]);

const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);

linkks.forEach((linkk) => {
 let nodeG1 = svg.append("g")
    .selectAll("circle")
    .data(linkk)
    .join("circle")
    .attr("cx", d => d.target.x)
    .attr("cy", d => d.target.y)
    .attr("fill", "none")
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
    })
    .attr("r", 6);
  let nodeG11 = svg.append("g")
    .selectAll("circle")
    .data(linkk)
    .join("circle")
    .attr("cx", d => d.source.x)
    .attr("cy", d => d.source.y)
    .attr("fill", "none")
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
    })
    .attr("r", 6);


  let nodeG2 = svg.append("g")
    .attr("font-family", "sans-serif")
    .attr("font-size", 14)
    .selectAll("text")
    .data(linkk)
    .join("text")
    .attr("class", "text")
    .attr("x", d => d.target.x + padding)
    .attr("y", d => d.target.y)
    .text(d => d.target.id )
    .attr("fill", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 2)))).toString(16);
    });
 
 let nodeG22 = svg.append("g")
    .attr("font-family", "sans-serif")
    .attr("font-size", 14)
    .selectAll("text")
    .data(linkk)
    .join("text")
    .attr("class", "text")
    .attr("x", d => d.source.x + padding)
    .attr("y", d => d.source.y)
    .text(d => d.source.id )
    .attr("fill", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
    });
 
  let nodeG = svg.append('g')
    .attr('class', 'node')
    .selectAll("path")
    .data(linkk)
    .join('path')
    .attr("class", "link")
    .attr("d", d3.linkHorizontal()
      .source(d => [d.xs, d.ys])
      .target(d => [d.xt, d.yt]))
    .attr("fill", "none")
    .attr("stroke-opacity", 0.325)
    .attr("stroke-width", 0.75)
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
    });
});
path {
  display: block;
  z-index: 0;
}

text,
circle {
  display: block;
  z-index: 1000;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

2 RubenHelsloot Oct 19 2020 at 13:19

Penso che molto di quello che hai fatto, in particolare riguardo alle discussioni sui dati, non fosse necessario, soprattutto da quando hai chiamato d3.hierarchy()e d3.cluster()dopo. L'ho sostituito con d3.stratify(che si occupa di dati gerarchici che non sono ancora nel formato corretto).

Ho anche sostituito d3.clustercon d3.tree()perché non mi era chiaro perché avresti voluto usare d3.clusterqui. I tuoi dati hanno più genitori, più radici e persino nodi mobili e d3 non è pensato per gestirlo. La mia soluzione alternativa è stata quella di allegare pseudonodi a ogni livello, in modo da assicurarmi che ci sia un solo nodo e che tutti i nodi siano al livello giusto in ogni momento. Per assicurarmi che i collegamenti siano stati disegnati correttamente, ho scritto una getLinksfunzione personalizzata , che può gestire più genitori.

Ho anche scritto un generatore di link personalizzato che disegna i link nel modo in cui li desideri. d3 non offre molta flessibilità qui, ma puoi usare il codice sorgente come ispirazione.


modificare

Ho cambiato la logica per essere più concentrato su quali "partner" hanno un figlio, quindi entrambi i collegamenti allo stesso bambino sono sullo stesso livello, come nella tua foto. Ho anche disegnato i nodi in base al numero di partner che hanno e ho dato a ogni collegamento un offset in modo che le linee siano più distinte.

Ho ordinato i nodi in modo che i veri creatori professionisti siano in alto (Zeus), il che offre una vista più equilibrata e meno affollata.

const margins = {
  top: 20,
  bottom: 300,
  left: 30,
  right: 100,
};

const height = 900;
const width = 900;

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

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

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

const levels = [
  [{
    id: 'Chaos',
  }],
  [{
    id: 'Gaea',
    parents: ['Chaos'],
  },
    {
      id: 'Uranus',
    },
  ],
  [{
    id: 'Oceanus',
    parents: ['Gaea', 'Uranus'],
  },
    {
      id: 'Thethys',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Pontus',
    },
    {
      id: 'Rhea',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Cronus',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Coeus',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Phoebe',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Crius',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Hyperion',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Iapetus',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Thea',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Themis',
      parents: ['Gaea', 'Uranus'],
    },
    {
      id: 'Mnemosyne',
      parents: ['Gaea', 'Uranus'],
    },
  ],
  [{
    id: 'Doris',
    parents: ['Oceanus', 'Thethys'],
  },
    {
      id: 'Neures',
      parents: ['Pontus', 'Gaea'],
    },
    {
      id: 'Dionne',
    },
    {
      id: 'Demeter',
      parents: ['Rhea', 'Cronus'],
    },
    {
      id: 'Hades',
      parents: ['Rhea', 'Cronus'],
    },
    {
      id: 'Hera',
      parents: ['Rhea', 'Cronus'],
    },
    {
      id: 'Alcmene',
    },
    {
      id: 'Zeus',
      parents: ['Rhea', 'Cronus'],
    },
    {
      id: 'Eris',
    },
    {
      id: 'Leto',
      parents: ['Coeus', 'Phoebe'],
    },
    {
      id: 'Amphitrite',
    },
    {
      id: 'Medusa',
    },
    {
      id: 'Poseidon',
      parents: ['Rhea', 'Cronus'],
    },
    {
      id: 'Hestia',
      parents: ['Rhea', 'Cronus'],
    },
  ],
  [{
    id: 'Thetis',
    parents: ['Doris', 'Neures'],
  },
    {
      id: 'Peleus',
    },
    {
      id: 'Anchises',
    },
    {
      id: 'Adonis',
    },
    {
      id: 'Aphrodite',
      parents: ['Zeus', 'Dionne'],
    },
    {
      id: 'Persephone',
      parents: ['Zeus', 'Demeter'],
    },
    {
      id: 'Ares',
      parents: ['Zeus', 'Hera'],
    },
    {
      id: 'Hephaestus',
      parents: ['Zeus', 'Hera'],
    },
    {
      id: 'Hebe',
      parents: ['Zeus', 'Hera'],
    },
    {
      id: 'Hercules',
      parents: ['Zeus', 'Alcmene'],
    },
    {
      id: 'Megara',
    },
    {
      id: 'Deianira',
    },
    {
      id: 'Eileithya',
      parents: ['Zeus', 'Hera'],
    },
    {
      id: 'Ate',
      parents: ['Zeus', 'Eris'],
    },
    {
      id: 'Leda',
    },
    {
      id: 'Athena',
      parents: ['Zeus'],
    },
    {
      id: 'Apollo',
      parents: ['Zeus', 'Leto'],
    },
    {
      id: 'Artemis',
      parents: ['Zeus', 'Leto'],
    },
    {
      id: 'Triton',
      parents: ['Poseidon', 'Amphitrite'],
    },
    {
      id: 'Pegasus',
      parents: ['Poseidon', 'Medusa'],
    },
    {
      id: 'Orion',
      parents: ['Poseidon'],
    },
    {
      id: 'Polyphemus',
      parents: ['Poseidon'],
    },
  ],
  [{
    id: 'Deidamia',
  },
    {
      id: 'Achilles',
      parents: ['Peleus', 'Thetis'],
    },
    {
      id: 'Creusa',
    },
    {
      id: 'Aeneas',
      parents: ['Anchises', 'Aphrodite'],
    },
    {
      id: 'Lavinia',
    },
    {
      id: 'Eros',
      parents: ['Hephaestus', 'Aphrodite'],
    },
    {
      id: 'Helen',
      parents: ['Leda', 'Zeus'],
    },
    {
      id: 'Menelaus',
    },
    {
      id: 'Polydueces',
      parents: ['Leda', 'Zeus'],
    },
  ],
  [{
    id: 'Andromache',
  },
    {
      id: 'Neoptolemus',
      parents: ['Deidamia', 'Achilles'],
    },
    {
      id: 'Aeneas(2)',
      parents: ['Creusa', 'Aeneas'],
    },
    {
      id: 'Pompilius',
      parents: ['Creusa', 'Aeneas'],
    },
    {
      id: 'Iulus',
      parents: ['Lavinia', 'Aeneas'],
    },
    {
      id: 'Hermione',
      parents: ['Helen', 'Menelaus'],
    },
  ],
];

levels.unshift([]);

// We add one pseudo node to every level to deal with parentless nodes
levels.forEach((l, i) => {
  l.forEach((n, j) => {
    n.level = i;
    if (n.parents !== undefined) {
      n.parent = n.parents[0];
    } else {
      n.parent = `pseudo-${i - 1}`; } }); l.unshift({ id: `pseudo-${i}`,
    parent: i > 0 ? `pseudo-${i - 1}` : "", level: i }); }); const nodes = levels.flat(); const colours = d3.scaleOrdinal() .domain(nodes.filter(n => n.parents) .map(n => n.parents.sort() .join("-"))) .range(d3.schemePaired); function getLinks(nodes) { return nodes .filter(n => n.data.parents !== undefined) .map(n => n.data.parents.map(p => ({ source: nodes.find(n => n.id === p), target: n }))) .flat(); } const offsetPerPartner = 3; const drawNodePath = d => { const radius = 5; // The number of partners determines the node height // But when a node has only one partner, // treat it the same as when it has zero const nPartners = (d.data.partners && d.data.partners.length > 1) ? d.data.partners.length : 0; // We want to centre each node const straightLineOffset = (nPartners * offsetPerPartner) / 2; const context = d3.path(); context.moveTo(-radius, 0); context.lineTo(-radius, -straightLineOffset); context.arc(0, -straightLineOffset, radius, -Math.PI, 0); context.lineTo(radius, straightLineOffset); context.arc(0, straightLineOffset, radius, 0, Math.PI); context.closePath(); return context + ""; }; const drawLinkCurve = (x0, y0, x1, y1, offset, radius) => { const context = d3.path(); context.moveTo(x0, y0); context.lineTo(x1 - 2 * radius - offset, y0); // If there is not enough space to draw two corners, reduce the corner radius if (Math.abs(y0 - y1) < 2 * radius) { radius = Math.abs(y0 - y1) / 2; } if (y0 < y1) { context.arcTo(x1 - offset - radius, y0, x1 - offset - radius, y0 + radius, radius); context.lineTo(x1 - offset - radius, y1 - radius); context.arcTo(x1 - offset - radius, y1, x1 - offset, y1, radius); } else if (y0 > y1) { context.arcTo(x1 - offset - radius, y0, x1 - offset - radius, y0 - radius, radius); context.lineTo(x1 - offset - radius, y1 + radius); context.arcTo(x1 - offset - radius, y1, x1 - offset, y1, radius); } context.lineTo(x1, y1); return context + ""; }; const partnershipsPerLevel = {}; const getPartnershipOffset = (parent, partner) => { let partnershipId, level; if(partner !== undefined) { // On every level, every relationship gets its own offset. If a relationship // spans multiple levels, the furthest level is chosen level = Math.max(parent.depth, partner.level); if(!partnershipsPerLevel[level]) { partnershipsPerLevel[level] = []; } partnershipId = [parent.id, partner.id].sort().join("-"); } else { level = parent.depth; if(!partnershipsPerLevel[level]) { partnershipsPerLevel[level] = []; } partnershipId = parent.id; } // Assume that the partnership already has a slot assigned const partnershipOffset = partnershipsPerLevel[level].indexOf(partnershipId); if(partnershipOffset === -1) { // Apparently not return partnershipsPerLevel[level].push(partnershipId) - 1; } return partnershipOffset; } const lineRadius = 10; const offsetStep = 5; const linkFn = link => { const thisParent = link.source; const partnerId = link.target.data.parents.find(p => p !== thisParent.id); const partners = thisParent.data.partners || []; // Let the first link start with this negative offset // But when a node has only one partner, // treat it the same as when it has zero const startOffset = (partners.length > 1) ? -(partners.length * offsetPerPartner) / 2 : 0; const partner = partners.find(p => p.id === partnerId); // Chaos has no partner, nor Zeus with Athena const nthPartner = partner !== undefined ? partners.indexOf(partner) : (partners || []).length; const partnershipOffset = getPartnershipOffset(thisParent, partner); return drawLinkCurve( thisParent.y, thisParent.x + startOffset + offsetPerPartner * nthPartner, link.target.y, link.target.x, offsetStep * partnershipOffset, lineRadius ); }; function draw(root) { // Now every node has had it's position set, we can draw them now const nodes = root.descendants() .filter(n => !n.id.startsWith("pseudo-")); const links = getLinks(nodes) .filter(l => !l.source.id.startsWith("pseudo-")); const link = graphGroup.selectAll(".link") .data(links); link.exit().remove(); link.enter() .append("path") .attr("class", "link") .merge(link) .attr("stroke", d => colours(d.target.data.parents.sort().join("-"))) .attr("d", linkFn); const node = graphGroup.selectAll(".node") .data(nodes); node.exit().remove(); const newNode = node.enter() .append("g") .attr("class", "node"); newNode.append("path") .attr("d", drawNodePath); newNode.append("text") .attr("dy", -3) .attr("x", 6); newNode.merge(node) .attr("transform", d => `translate(${d.y},${d.x})`)
    .selectAll("text")
    .text(d => d.id);
}

const root = d3.stratify()
  .parentId(d => d.parent)
  (nodes);

// Map the different sets of parents,
// assigning each parent an array of partners
getLinks(root.descendants())
  .filter(l => l.target.data.parents)
  .forEach(l => {
    const parentNames = l.target.data.parents;
    if (parentNames.length > 1) {
      const parentNodes = parentNames.map(p => nodes.find(n => n.id === p));

      parentNodes.forEach(p => {
        if (!p.partners) {
          p.partners = [];
        }
        parentNodes
          .filter(n => n !== p && !p.partners.includes(n))
          .forEach(n => {
            p.partners.push(n);
          });
      });
    }
  });

// Take nodes with more partners first,
// also counting the partners of the children
root
  .sum(d => (d.value || 0) + (d.partners || []).length)
  .sort((a, b) => b.value - a.value);

const tree = d3.tree()
  .size([height, width])
  .separation((a, b) => {
    // More separation between nodes with many children
    const totalPartners = (a.data.partners || []).length + (b.data.partners || []).length;
    return 1 + (totalPartners / 5);
  });

draw(tree(root));
.link {
  fill: none;
  stroke-width: 2;
}

.node path {
  stroke: black;
  stroke-width: 3;
  fill: white;
}
<script src="https://d3js.org/d3.v5.js"></script>