How to Connect Two Parent Node into One Child Node and How to Make a Tooltip for Each Node in the Tree Pragmatically? in D3 Js (Svg)

How can I connect two parent node into one child node and how to make a tooltip for each node in the tree pragmatically? in D3 js (SVG)

The simplest thing you can do is just assign a parent at random, then use that parent to position the nodes, and draw the links yourself.

var svg = d3
.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(50,50)");

//tree data
var data = [
{ child: "Alice", parents: [] },
{ child: "Bob", parents: [] },
{ child: "Carol", parents: [] },
{ child: "Dave", parents: ["Alice", "Bob"] },
{ child: "Eve", parents: ["Alice", "Bob"] },
{ child: "Francis", parents: ["Bob", "Carol"] },
{ child: "Graham", parents: ["Carol"] },
{ child: "Hugh", parents: ["Eve", "Graham"] },
];

// Process the nodes, add a pseudo root node so we don't have
// multiple roots
data.forEach(function(d) {
d.parentId = d.parents.length > 0 ? d.parents[0] : "root";
});
data.unshift({ child: "root", parentId: "" });

//to construct
var dataStructure = d3
.stratify()
.id(function (d) {
return d.child;
})
.parentId(function (d) {
return d.parentId;
})(data);

//to define the size of the structure tree
var treeStructure = d3.tree().size([500, 300]);
var root = treeStructure(dataStructure);

var nodes = root.descendants()
.filter(function(d) { return d.id !== "root"; });

// Custom way to get all links we need to draw
var links = [];
nodes.forEach(function(node) {
node.data.parents.forEach(function(parentId) {
var parentNode = nodes.find(function(d) { return d.id === parentId; });
links.push({
source: parentNode,
target: node,
});
});
});

//to make the connections curves
var connections = svg.append("g").selectAll("path").data(links);
connections
.enter()
.append("path")
.attr("d", function (d) {
return (
"M" +
d.source.x +
"," +
d.source.y +
" C " +
d.source.x +
"," +
(d.source.y + d.target.y) / 2 +
" " +
d.target.x +
"," +
(d.source.y + d.target.y) / 2 +
" " +
d.target.x +
"," +
d.target.y
);
});

//creating the circles with data info
var circles = svg
.append("g")
.selectAll("circle")
.data(nodes);

//placing the circles
circles
.enter()
.append("circle")
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", 7)
.append("text");

//names
var names = svg.append("g").selectAll("text").data(nodes);
names
.enter()
.append("text")
.text(function (d) {
return d.id;
})
.attr("x", function (d) {
return d.x + 7;
})
.attr("y", function (d) {
return d.y + 4;
});
circle {
fill: rgb(88, 147, 0);
}

path {
fill: none;
stroke: black;
}
<script src="https://d3js.org/d3.v6.min.js"></script>

d3.js multiple relationship visual / linkHorizontal() / tangled tree

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>

How to add a tooltip to an svg graphic?

You can use the title element as Phrogz indicated. There are also some good tooltips like jQuery's Tipsy http://onehackoranother.com/projects/jquery/tipsy/ (which can be used to replace all title elements), Bob Monteverde's nvd3 or even the Twitter's tooltip from their Bootstrap http://twitter.github.com/bootstrap/

How do you create a family tree in d3.js?

My approach is as under:

Lets take the example you have illustrated in the attached figure:

Jenny Of Oldstones is also a the child of Aegon V but the difference between this child and other children of Aegon V is that in this case I am not drawing the link between it.

This is done by setting the node as no_parent: true in the node JSON
example:

//Here Q will not have a parent
{
name: "Q",
id: 16,
no_parent: true
}

In the code check the _elbow function_ this does the job of not drawing the line between it and its parent:

if (d.target.no_parent) {
return "M0,0L0,0";
}

Next scenario is the link going between Node Aerys II and Rahella this node has its set of children.

  • I have created a node between them which is marked as hidden: true,
  • I make the display:none for such node. It appears that the children are coming from the line between node Aerys II and Rahella

JSON Example:

//this node will not be displayed
{ name: "",
id: 2,
no_parent: true,
hidden: true,
children: [....]

}

In the code check the place where I make the rectangles, the code below hides the node:

    .attr("display", function (d) {
if (d.hidden) {
return "none"
} else {
return ""
};
})

Full code is in here:
http://jsfiddle.net/cyril123/0vbtvoon/22/

In the example above, I have made the use of node names A/B/C... but you can change it as per you requirements. You will need to center the text.

I have added comments to the code to help you understand the flow.
Just in case you are not clear on any point please comment I ll be happy to clarify.

Show data on mouseover of circle

I assume that what you want is a tooltip. The easiest way to do this is to append an svg:title element to each circle, as the browser will take care of showing the tooltip and you don't need the mousehandler. The code would be something like

vis.selectAll("circle")
.data(datafiltered).enter().append("svg:circle")
...
.append("svg:title")
.text(function(d) { return d.x; });

If you want fancier tooltips, you could use tipsy for example. See here for an example.



Related Topics



Leave a reply



Submit