Skip to content

How Can I highlight the parents names and the connection lines when I hover on their child in family tree

I’m creating family tree using d3.js and i want to do 2 things

  1. 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
  2. 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>

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 ids 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>