d3.js Mehrfachbeziehung Visual / LinkHorizontal () / Tangled Tree
Ich versuche, ein Bild nachzuahmen, das mehrere Beziehungen nach Zeitraum darstellt, wie folgt (Zeitraum = Generierung):
Meine Bemühungen haben sich jedoch bisher nicht ausgeweitet. Ich erhalte immer noch eine leere Ausgabe im Browser. Fest codierte Daten und Code im 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>
Soweit ich weiß, sind alle Teile vorhanden. Ich habe meine Daten in levelsund habe dann die erforderlichen Hierarchiekoordinaten mit folgenden Informationen gerungen:
var links = []
nodes.forEach(d => {
d.parents.forEach(p => links.push({source: d, bundle: d.bundle, target: p}))
})
und
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');
Von hier aus ging ich mit d3.linkHorizontal()für meine Link-Funktion:
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');
Konzeptionell sehe ich nicht, wie das Einbeziehen mehrerer Beziehungen pro Knoten die Dinge verändert. Und da im Konsolenprotokoll keine Fehler vorliegen, bin ich mir nicht sicher, wie ich weitere Fehler beheben soll.
Frage
Was hindert mein Bild daran, wie gewünscht zu rendern (im Bild oben)? Möchte wenn möglich eine exakte Nachbildung.
Bearbeiten
Hier ist das Bild sichtbar, wenn dies hilfreich ist, aber nicht als eigenständiges Bild angesehen werden kann.
https://observablehq.com/@nitaku/tangled-tree-visualization-ii?collection=@nitaku/tangled-trees
Antworten
An das svgElement wird nichts angehängt , außer graphGroup. Anscheinend wird root.links()ein leeres Array zurückgegeben und nichts wird an das angehängt svg. Dies ist auch der Grund, warum Sie keine Fehler erhalten.
Indem Sie dieses Array erstellen und darauf iterieren, wird die Grundform, die Sie in Ihrem Baum erreichen möchten, implementiert, wenn Sie auch Folgendes ändern:
.attr("d", d3.linkHorizontal()
.x(function(d) { return d.y; })
.y(function(d) { return d.x; }))
mit:
.attr("d", d3.linkHorizontal()
.source(d => [d.xs,d.ys] )
.target(d => [d.xt,d.yt]))
Die Grundform des Baums, den Sie implementieren möchten, sehen Sie im folgenden Snippet. Versuchen Sie herauszufinden, ob dieses Beispiel dazu beitragen kann, Ihren Baum wie gewünscht zu gestalten.
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>
Ich denke, vieles, was Sie getan haben, insbesondere in Bezug auf Daten-Wrangling, war nicht notwendig, insbesondere seit Sie angerufen haben d3.hierarchy()und d3.cluster()danach. Ich habe dies durch ersetzt d3.stratify(das sich mit hierarchischen Daten befasst, die noch nicht das richtige Format haben).
Ich habe auch ersetzt d3.clustermit , d3.tree()denn es war mir unklar, warum Sie verwenden möchten , würde d3.clusterhier. Ihre Daten haben mehrere Eltern, mehrere Wurzeln und sogar schwebende Knoten, und d3 ist nicht dazu gedacht, damit umzugehen. Meine Problemumgehung bestand darin, Pseudo-Knoten an jede Ebene anzuhängen, um sicherzustellen, dass nur ein Knoten vorhanden ist und sich alle Knoten jederzeit auf der richtigen Ebene befinden. Um sicherzustellen, dass die Links korrekt gezeichnet wurden, habe ich eine benutzerdefinierte getLinksFunktion geschrieben, die mehrere Eltern behandeln kann.
Ich habe auch einen benutzerdefinierten Linkgenerator geschrieben, der die Links so zeichnet, wie Sie es möchten. d3 bietet hier nicht viel Flexibilität, aber Sie können den Quellcode als Inspiration verwenden.
Bearbeiten
Ich habe die Logik geändert, um mich mehr darauf zu konzentrieren, welche "Partner" ein Kind haben, sodass beide Links zu demselben Kind auf derselben Ebene liegen - wie in Ihrem Bild. Ich habe auch die Knoten basierend auf der Anzahl ihrer Partner gezeichnet und jedem Link einen Versatz gegeben, damit die Linien deutlicher werden.
Ich habe die Knoten so sortiert, dass die echten Pro-Schöpfer oben sind (Zeus), was eine ausgewogenere und weniger überfüllte Ansicht ergibt.
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>