最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - d3.js multiple relationship visuallinkHorizontal()tangled tree - Stack Overflow

programmeradmin2浏览0评论

I am trying to mimic a visual that depicts multiple relationships by time period, like this (time period = generation):

However, my efforts have not panned out thus far; I'm still getting blank output in the browser. Hard coded data and code in the 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']}
    ]
  ]

  // prepute 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])
  })

  // prepute 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
  })

  // press 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=".v5.min.js"></script>

I am trying to mimic a visual that depicts multiple relationships by time period, like this (time period = generation):

However, my efforts have not panned out thus far; I'm still getting blank output in the browser. Hard coded data and code in the 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']}
    ]
  ]

  // prepute 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])
  })

  // prepute 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
  })

  // press 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/d3.v5.min.js"></script>

As far as I know, all the pieces are in place. I have my data in levels and then have wrangled the necessary hierarchy coordinates using:

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

and

  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');

From here, I went with d3.linkHorizontal() for my link function:

      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');

Conceptually, I don't see how including multiple relationships per node changes things. And, absent any errors in the console log, I'm not sure how to troubleshoot further.

Question

What is preventing my visual from rendering as desired (in the picture above)? Would like exact replica if possible.

Edit

Here is the visual on observable if that helps, but can't be viewed as a standalone visual.

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

Share Improve this question edited Oct 26, 2020 at 2:04 Arash Howaida asked Oct 19, 2020 at 7:46 Arash HowaidaArash Howaida 2,6174 gold badges23 silver badges56 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 3

I think a lot of what you did, specifically around data wrangling, was not necessary, especially since you called d3.hierarchy() and d3.cluster() afterwards. I've replaced this with d3.stratify (which deals with hierarchical data that is not yet in the right format).

I've also replaced d3.cluster with d3.tree() because it was unclear to me why you'd want to use d3.cluster here. Your data has multiple parents, multiple roots and even floating nodes, and d3 is not meant to deal with that. My workaround has been to attach pseudonodes to every level, so as to make sure that there is only one node and that all nodes are at the right level at all times. To make sure the links were drawn correctly, I've written a custom getLinks function, that can deal with multiple parents.

I've also written a custom link generator that draws the links somewhat in the way that you want them. d3 doesn't offer much of flexibility here, but you can use the source code for inspiration.


Edit

I've changed the logic to be more focused on which "partners" got a child, so both links to the same child are on the same level - like in your picture. I've also drawn the nodes based on how many partners they have, and have given every link an offset so the lines are more distinct.

I've sorted the nodes so that the real pro-creators are at the top (Zeus), which gives a more balanced and less crowded view.

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/d3.v5.js"></script>

If anyone in the future needs it, here is my what I got working:

<!DOCTYPE html>
<html>
<head>
  <script src="https://d3js/d3.v5.js"></script>
  <script src="https://d3js/d3-scale.v3.min.js"></script>
  <script src="https://d3js/d3-scale-chromatic.v3.min.js"></script>
  <script src="https://cdn.jsdelivr/npm/lodash/lodash.min.js"></script>
</head>
<body>
  <script>
    // Define svg as a utility for tagged template literals
    const svg = (strings, ...values) => {
      return strings.reduce((result, str, i) => result + str + (values[i] || ''), '');
    };

    renderChart = (data, options={}) => {
      options.color ||= (d, i) => color(i)
      
      const tangleLayout = constructTangleLayout(_.cloneDeep(data), options);

      return svg`<svg width="${tangleLayout.layout.width}" height="${
        tangleLayout.layout.height
      }" style="background-color: ${background_color}">
      <style>
        text {
          font-family: sans-serif;
          font-size: 10px;
        }
        .node {
          stroke-linecap: round;
        }
        .link {
          fill: none;
        }
      </style>

      ${tangleLayout.bundles.map((b, i) => {
        let d = b.links
          .map(
        l => `
          M${l.xt} ${l.yt}
          L${l.xb - l.c1} ${l.yt}
          A${l.c1} ${l.c1} 90 0 1 ${l.xb} ${l.yt + l.c1}
          L${l.xb} ${l.ys - l.c2}
          A${l.c2} ${l.c2} 90 0 0 ${l.xb + l.c2} ${l.ys}
          L${l.xs} ${l.ys}`
          )
          .join("");
        return `
          <path class="link" d="${d}" stroke="${background_color}" stroke-width="5"/>
          <path class="link" d="${d}" stroke="${options.color(b, i)}" stroke-width="2"/>
        `;
      })}

      ${tangleLayout.nodes.map(
        n => `
        <path class="selectable node" data-id="${
          n.id
        }" stroke="black" stroke-width="8" d="M${n.x} ${n.y - n.height / 2} L${
          n.x
        } ${n.y + n.height / 2}"/>
        <path class="node" stroke="white" stroke-width="4" d="M${n.x} ${n.y -
          n.height / 2} L${n.x} ${n.y + n.height / 2}"/>

        <text class="selectable" data-id="${n.id}" x="${n.x + 4}" y="${n.y -
          n.height / 2 -
          4}" stroke="${background_color}" stroke-width="2">${n.id}</text>
        <text x="${n.x + 4}" y="${n.y -
          n.height / 2 -
          4}" style="pointer-events: none;">${n.id}</text>
      `
      )}

      </svg>`;
    }
    
    const data = [
      [{ 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'] }
      ]
    ];
    
    constructTangleLayout = (levels, options={}) => {
      // prepute 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]
        );
      });

      // prepute 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('-X-');
          if (id in index) {
        index[id].parents = index[id].parents.concat(n.parents);
          } else {
        index[id] = { id: id, parents: n.parents.slice(), level: i, span: i - d3.min(n.parents, p => p.level) };
          }
          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.sort((a,b) => d3.descending(d3.max(a, d => d.span), d3.max(b, d => d.span)))
        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 min_family_height = 22;
      
      options.c ||= 16;
      const c = options.c;
      options.bigc ||= node_width+c;

      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 =
        d3.max(b.parents, d => d.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.yb = l.bundle.y;
        l.xs = l.source.x;
        l.ys = l.source.y;
      });
      
      // press 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 - 2*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 ? Math.min(options.bigc, l.xb-l.xt, l.yb-l.yt)-c : c;
        l.c2 = c;
      });

      var layout = {
        width: d3.max(nodes, n => n.x) + node_width + 2 * padding,
        height: d3.max(nodes, n => n.y) + node_height / 2 + 2 * padding,
        node_height,
        node_width,
        bundle_width,
        level_y_padding,
        metro_d
      };

      return { levels, nodes, nodes_index, links, bundles, layout };
    };
    
    color = d3.scaleOrdinal(d3.schemeDark2);
    
    background_color = 'white';
    
    document.body.innerHTML = renderChart(data);
  </script>
</body>
</html>

Nothing is appended in the svg element except graphGroup. Apparently root.links() return an empty array and nothing is appended in the svg. That is also the reason why you are not getting any errors.

By creating this array and iterating on it the basic shape that you want to achieve in your tree is implemented if you change also:

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

with:

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

The basic shape of the tree you want to implement can be seen in the below snippet. Try to see if this example could help in styling your tree as desired.

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']
    }
  ]
]

// prepute 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])
})

// prepute 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
})

// press 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/d3.v5.min.js"></script>

发布评论

评论列表(0)

  1. 暂无评论