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']}
]
]
// 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>
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.
What is preventing my visual from rendering as desired (in the picture above)? Would like exact replica if possible.
Here is the visual on observable if that helps, but can't be viewed as a standalone visual.
https://observablehq.com/@nitaku/tangled-tree-visualization-ii?collection=@nitaku/tangled-trees
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']
}
]
]
// 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>