d3.js relation multiple visual / linkHorizontal () / arbre emmêlé

Oct 19 2020

J'essaie d'imiter un visuel qui représente plusieurs relations par période, comme ceci (période = génération):

Cependant, mes efforts n'ont pas abouti jusqu'ici; Je reçois toujours une sortie vierge dans le navigateur. Données et code codés en dur dans l'extrait:

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>

Autant que je sache, toutes les pièces sont en place. J'ai mes données levelset j'ai ensuite arrangé les coordonnées hiérarchiques nécessaires en utilisant:

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

et

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

De là, je suis allé avec d3.linkHorizontal()pour ma fonction de lien:

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

Conceptuellement, je ne vois pas comment l'inclusion de plusieurs relations par nœud change les choses. Et, en l'absence d'erreurs dans le journal de la console, je ne sais pas comment résoudre les problèmes plus loin.

Question

Qu'est-ce qui empêche mon visuel de s'afficher comme souhaité (dans l'image ci-dessus)? Voudrait une réplique exacte si possible.

Éditer

Voici le visuel sur observable si cela aide, mais ne peut pas être considéré comme un visuel autonome.

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

Réponses

1 PeterDarmis Oct 28 2020 at 12:08

Rien n'est ajouté dans l' svgélément sauf graphGroup. Apparemment, root.links()retourne un tableau vide et rien n'est ajouté dans le fichier svg. C'est aussi la raison pour laquelle vous n'obtenez aucune erreur.

En créant ce tableau et en l'itérant, la forme de base que vous souhaitez obtenir dans votre arbre est implémentée si vous changez également:

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

avec:

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

La forme de base de l'arborescence que vous souhaitez implémenter est visible dans l'extrait ci-dessous. Essayez de voir si cet exemple pourrait vous aider à styliser votre arbre comme vous le souhaitez.

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

Je pense qu'une grande partie de ce que vous avez fait, en particulier en ce qui concerne le traitement des données, n'était pas nécessaire, surtout depuis que vous avez appelé d3.hierarchy()et d3.cluster()après. J'ai remplacé cela par d3.stratify(qui traite des données hiérarchiques qui ne sont pas encore au bon format).

J'ai également remplacé d3.clusterpar d3.tree()parce que je ne savais pas pourquoi vous voudriez l'utiliser d3.clusterici. Vos données ont plusieurs parents, plusieurs racines et même des nœuds flottants, et d3 n'est pas destiné à gérer cela. Ma solution de contournement a été d'attacher des pseudonodes à chaque niveau, afin de m'assurer qu'il n'y a qu'un seul nœud et que tous les nœuds sont au bon niveau à tout moment. Pour m'assurer que les liens ont été correctement dessinés, j'ai écrit une getLinksfonction personnalisée , qui peut gérer plusieurs parents.

J'ai également écrit un générateur de liens personnalisé qui dessine les liens un peu comme vous le souhaitez. d3 n'offre pas beaucoup de flexibilité ici, mais vous pouvez utiliser le code source pour vous inspirer.


Éditer

J'ai changé la logique pour me concentrer davantage sur les «partenaires» qui ont un enfant, donc les deux liens vers le même enfant sont au même niveau - comme dans votre image. J'ai également dessiné les nœuds en fonction du nombre de partenaires dont ils disposent et j'ai donné à chaque lien un décalage pour que les lignes soient plus distinctes.

J'ai trié les nœuds pour que les vrais pro-créateurs soient au sommet (Zeus), ce qui donne une vue plus équilibrée et moins encombrée.

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>