I’m creating family tree using d3.js and i want to do 2 things
- how can i highlight the parents names and the connection lines when I hover on their child like this image when i hover on axelenter image description here
- how can i determine the last child in each branch so i can give it unique style or add icon after it eg: tree leaf like this image enter image description here
Here is a snippet of my code:
const familyData = [ { _id: '60da7d37b8ca2d2590f0b713', child: 'William', parent: '', parentId: null }, { _id: '60da7d7f6a89ad1fecc905e9', child: 'James', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7d9619156a6d90874aa1', child: 'Henry', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7db3c1f2f27368395212', child: 'Michael', parent: 'James', parentId: '60da7d7f6a89ad1fecc905e9' }, { _id: '60da7dd32796ae5cbc0e1810', child: 'Ethan', parent: 'James', parentId: '60da7d7f6a89ad1fecc905e9' }, { _id: '60da7df79f58c4028cb21d06', child: 'Jacob', parent: 'Henry', parentId: '60da7d9619156a6d90874aa1' }, { _id: '60da7e149cf24f1d20167c14', child: 'Jack', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7e2add5413458427c4b2', child: 'Joseph', parent: 'Jack', parentId: '60da7e149cf24f1d20167c14' }, { _id: '60da7e48fec03d0b1c2d10d3', child: 'Asher', parent: 'Joseph', parentId: '60da7e2add5413458427c4b2' }, { _id: '60da7e5c8cc6f66264b23e70', child: 'Leo', parent: 'Ethan', parentId: '60da7dd32796ae5cbc0e1810' }, { _id: '60da7e89cefbdd785cec5ada', child: 'Isaac', parent: 'Leo', parentId: '60da7e5c8cc6f66264b23e70' }, { _id: '60da7e93ed9bd0402487e5c8', child: 'Charles', parent: 'Leo', parentId: '60da7e5c8cc6f66264b23e70' }, { _id: '60da7ea006b3694914c99ee0', child: 'Caleb', parent: 'Michael', parentId: '60da7db3c1f2f27368395212' }, { _id: '60da7eab6a06e223e42b5d65', child: 'Ryan', parent: 'Michael', parentId: '60da7db3c1f2f27368395212' }, { _id: '60da7e6b05ff5f0468d8e835', child: 'Thomas', parent: 'Jacob', parentId: '60da7df79f58c4028cb21d06' }, { _id: '60da7eb5b7a93714303ef471', child: 'Aaron', parent: 'Thomas', parentId: '60da7e6b05ff5f0468d8e835' }, { _id: '60da7ebcf21a2a44503b7596', child: 'Axel', parent: 'Thomas', parentId: '60da7e6b05ff5f0468d8e835' } ] const dataStructure = d3.stratify() .id(d => d._id) .parentId(d => d.parentId)(familyData) const treeStructure = d3.tree() .size([500,300]) let root = treeStructure(dataStructure) console.log(root.descendants()); console.log(root.links()); const svg = d3.select('svg') .attr('width',600) .attr('height',600) const nodes = svg.append('g') .attr('transform','translate(50,50)') .selectAll('circle') .data(root.descendants()) .enter() .append('circle') .attr('cx', d => d.x) .attr('cy', d => d.y) .attr('r', 3) .attr('fill', function(d){ if(d.depth === 0) return 'black' else if (d.depth === 1) return 'red' else if (d.depth === 2) return 'green' else if (d.depth === 3) return 'magenta' else return 'brown' }) const connections = svg.append('g') .attr('transform','translate(50,50)') .selectAll('path') .data(root.links()) .enter() .append('path') .attr('d', d => `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}`) const names = svg.append('g') .attr('transform','translate(50,50)') .selectAll('text') .data(root.descendants()) .enter() .append('text') .text(d => d.data.child) .attr('x', d => d.x + 8) .attr('y', d => d.y + 2) .style('font-size', '1rem') .on('mouseover', function(e,d){ d3.select(this) .transition() .duration('100') .attr('opacity', 1) .style('font-size','2rem') d3.selectAll('text').attr('opacity', '0.3') d3.selectAll('circle').attr('opacity', '0.3') }) .on('mouseout', function(d){ d3.select(this) .style('font-size','1rem') d3.selectAll('text').attr('opacity', '1') d3.selectAll('circle').attr('opacity', '1') })
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://unpkg.com/d3@7.0.0/dist/d3.min.js"></script> <script src='try.js' defer></script> <style> path { fill:transparent; stroke:teal; } text { cursor: pointer; } </style> <title>Document</title> </head> <body> <svg></svg> </body> </html>
Advertisement
Answer
To highlight the ancestor paths, you need to add id
attributes to the connections
:
.attr('id', d => "link_" + d.target.data._id)
And the names
:
.attr('id', d => d.data.child)
Then in the mouseover
and mouseout
events call functions to highlight and un-highlight the paths. You need the id
s to refer to in the highlightPath
function:
function unhighlightPath(event, d) { //reset all nodes color d3.selectAll("path").style("stroke", "teal"); } function highlightPath(event, d) { // select link from hovered label to immediate parent d3.select("#link_" + d.data._id).style("stroke", "red"); // keep going up until no more parents while (d.parent) { if (d.parent != "null") { d3.selectAll("#link_"+d.parent.data._id).style("stroke", "red") } d = d.parent; } }
To identify if a node is a leaf node, test the presence of .children
. I adapted your code to colour parents red and children blue. You can use this test and add icons and styling per your requirements.
.attr('fill', function(d){ //if(d.depth === 0) return 'black' //else if (d.depth === 1) return 'red' //else if (d.depth === 2) return 'green' //else if (d.depth === 3) return 'magenta' //else return 'brown' if (d.children) { // not leaf nodes return "red"; } else { // leaf nodes // do some stuff like icons, extra styling return "blue"; // for the node colour }
Your code adapted:
const familyData = [ { _id: '60da7d37b8ca2d2590f0b713', child: 'William', parent: '', parentId: null }, { _id: '60da7d7f6a89ad1fecc905e9', child: 'James', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7d9619156a6d90874aa1', child: 'Henry', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7db3c1f2f27368395212', child: 'Michael', parent: 'James', parentId: '60da7d7f6a89ad1fecc905e9' }, { _id: '60da7dd32796ae5cbc0e1810', child: 'Ethan', parent: 'James', parentId: '60da7d7f6a89ad1fecc905e9' }, { _id: '60da7df79f58c4028cb21d06', child: 'Jacob', parent: 'Henry', parentId: '60da7d9619156a6d90874aa1' }, { _id: '60da7e149cf24f1d20167c14', child: 'Jack', parent: 'William', parentId: '60da7d37b8ca2d2590f0b713' }, { _id: '60da7e2add5413458427c4b2', child: 'Joseph', parent: 'Jack', parentId: '60da7e149cf24f1d20167c14' }, { _id: '60da7e48fec03d0b1c2d10d3', child: 'Asher', parent: 'Joseph', parentId: '60da7e2add5413458427c4b2' }, { _id: '60da7e5c8cc6f66264b23e70', child: 'Leo', parent: 'Ethan', parentId: '60da7dd32796ae5cbc0e1810' }, { _id: '60da7e89cefbdd785cec5ada', child: 'Isaac', parent: 'Leo', parentId: '60da7e5c8cc6f66264b23e70' }, { _id: '60da7e93ed9bd0402487e5c8', child: 'Charles', parent: 'Leo', parentId: '60da7e5c8cc6f66264b23e70' }, { _id: '60da7ea006b3694914c99ee0', child: 'Caleb', parent: 'Michael', parentId: '60da7db3c1f2f27368395212' }, { _id: '60da7eab6a06e223e42b5d65', child: 'Ryan', parent: 'Michael', parentId: '60da7db3c1f2f27368395212' }, { _id: '60da7e6b05ff5f0468d8e835', child: 'Thomas', parent: 'Jacob', parentId: '60da7df79f58c4028cb21d06' }, { _id: '60da7eb5b7a93714303ef471', child: 'Aaron', parent: 'Thomas', parentId: '60da7e6b05ff5f0468d8e835' }, { _id: '60da7ebcf21a2a44503b7596', child: 'Axel', parent: 'Thomas', parentId: '60da7e6b05ff5f0468d8e835' } ] const dataStructure = d3.stratify() .id(d => d._id) .parentId(d => d.parentId)(familyData) const treeStructure = d3.tree().size([500,300]) let root = treeStructure(dataStructure) //console.log(root.descendants()); //console.log(root.links()); const svg = d3.select('svg') .attr('width',600) .attr('height',600) const nodes = svg.append('g') .attr('transform','translate(50,50)') .selectAll('circle') .data(root.descendants()) .enter() .append('circle') .attr('cx', d => d.x) .attr('cy', d => d.y) .attr('r', 3) .attr('fill', function(d){ //if(d.depth === 0) return 'black' //else if (d.depth === 1) return 'red' //else if (d.depth === 2) return 'green' //else if (d.depth === 3) return 'magenta' //else return 'brown' if (d.children) { // not leaf nodes return "red"; } else { // leaf nodes // do some stuff like icons, extra styling return "blue"; // for the node colour } }) const connections = svg.append('g') .attr('transform','translate(50,50)') .selectAll('path') .data(root.links()) .enter() .append('path') .attr('id', d => "link_" + d.target.data._id) .attr('d', d => `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}`) const names = svg.append('g') .attr('transform','translate(50,50)') .selectAll('text') .data(root.descendants()) .enter() .append('text') .attr('id', d => d.data.child) .text(d => d.data.child) .attr('x', d => d.x + 8) .attr('y', d => d.y + 2) .style('font-size', '1rem') .on('mouseover', function(e,d){ d3.select(this) .transition() .duration('100') .attr('opacity', 1) .style('font-size','2rem') d3.selectAll('text').attr('opacity', '0.3') d3.selectAll('circle').attr('opacity', '0.3'); highlightPath(e, d); }) .on('mouseout', function(e, d){ d3.select(this) .style('font-size','1rem') d3.selectAll('text').attr('opacity', '1') d3.selectAll('circle').attr('opacity', '1'); unhighlightPath(e, d) }); // ancestor paths function unhighlightPath(event, d) { //reset all nodes color d3.selectAll("path").style("stroke", "teal"); } function highlightPath(event, d) { // select link from hovered label to immediate parent d3.select("#link_" + d.data._id).style("stroke", "red"); // keep going up until no more parents while (d.parent) { if (d.parent != "null") { d3.selectAll("#link_"+d.parent.data._id).style("stroke", "red") } d = d.parent; } }
path { fill:transparent; stroke:teal; } text { cursor: pointer; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script> <svg></svg>