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:
JavaScript
x
174
174
1
const familyData = [
2
{
3
_id: '60da7d37b8ca2d2590f0b713',
4
child: 'William',
5
parent: '',
6
parentId: null
7
},
8
{
9
_id: '60da7d7f6a89ad1fecc905e9',
10
child: 'James',
11
parent: 'William',
12
parentId: '60da7d37b8ca2d2590f0b713'
13
},
14
{
15
_id: '60da7d9619156a6d90874aa1',
16
child: 'Henry',
17
parent: 'William',
18
parentId: '60da7d37b8ca2d2590f0b713'
19
},
20
{
21
_id: '60da7db3c1f2f27368395212',
22
child: 'Michael',
23
parent: 'James',
24
parentId: '60da7d7f6a89ad1fecc905e9'
25
},
26
{
27
_id: '60da7dd32796ae5cbc0e1810',
28
child: 'Ethan',
29
parent: 'James',
30
parentId: '60da7d7f6a89ad1fecc905e9'
31
},
32
{
33
_id: '60da7df79f58c4028cb21d06',
34
child: 'Jacob',
35
parent: 'Henry',
36
parentId: '60da7d9619156a6d90874aa1'
37
},
38
{
39
_id: '60da7e149cf24f1d20167c14',
40
child: 'Jack',
41
parent: 'William',
42
parentId: '60da7d37b8ca2d2590f0b713'
43
},
44
{
45
_id: '60da7e2add5413458427c4b2',
46
child: 'Joseph',
47
parent: 'Jack',
48
parentId: '60da7e149cf24f1d20167c14'
49
},
50
{
51
_id: '60da7e48fec03d0b1c2d10d3',
52
child: 'Asher',
53
parent: 'Joseph',
54
parentId: '60da7e2add5413458427c4b2'
55
},
56
{
57
_id: '60da7e5c8cc6f66264b23e70',
58
child: 'Leo',
59
parent: 'Ethan',
60
parentId: '60da7dd32796ae5cbc0e1810'
61
},
62
{
63
_id: '60da7e89cefbdd785cec5ada',
64
child: 'Isaac',
65
parent: 'Leo',
66
parentId: '60da7e5c8cc6f66264b23e70'
67
},
68
{
69
_id: '60da7e93ed9bd0402487e5c8',
70
child: 'Charles',
71
parent: 'Leo',
72
parentId: '60da7e5c8cc6f66264b23e70'
73
},
74
{
75
_id: '60da7ea006b3694914c99ee0',
76
child: 'Caleb',
77
parent: 'Michael',
78
parentId: '60da7db3c1f2f27368395212'
79
},
80
{
81
_id: '60da7eab6a06e223e42b5d65',
82
child: 'Ryan',
83
parent: 'Michael',
84
parentId: '60da7db3c1f2f27368395212'
85
},
86
{
87
_id: '60da7e6b05ff5f0468d8e835',
88
child: 'Thomas',
89
parent: 'Jacob',
90
parentId: '60da7df79f58c4028cb21d06'
91
},
92
{
93
_id: '60da7eb5b7a93714303ef471',
94
child: 'Aaron',
95
parent: 'Thomas',
96
parentId: '60da7e6b05ff5f0468d8e835'
97
},
98
{
99
_id: '60da7ebcf21a2a44503b7596',
100
child: 'Axel',
101
parent: 'Thomas',
102
parentId: '60da7e6b05ff5f0468d8e835'
103
}
104
]
105
106
const dataStructure = d3.stratify()
107
.id(d => d._id)
108
.parentId(d => d.parentId)(familyData)
109
110
const treeStructure = d3.tree()
111
.size([500,300])
112
let root = treeStructure(dataStructure)
113
114
console.log(root.descendants());
115
console.log(root.links());
116
117
const svg = d3.select('svg')
118
.attr('width',600)
119
.attr('height',600)
120
121
122
123
const nodes = svg.append('g')
124
.attr('transform','translate(50,50)')
125
.selectAll('circle')
126
.data(root.descendants())
127
.enter()
128
.append('circle')
129
.attr('cx', d => d.x)
130
.attr('cy', d => d.y)
131
.attr('r', 3)
132
.attr('fill', function(d){
133
if(d.depth === 0) return 'black'
134
else if (d.depth === 1) return 'red'
135
else if (d.depth === 2) return 'green'
136
else if (d.depth === 3) return 'magenta'
137
else return 'brown'
138
})
139
140
const connections = svg.append('g')
141
.attr('transform','translate(50,50)')
142
.selectAll('path')
143
.data(root.links())
144
.enter()
145
.append('path')
146
.attr('d', d => `M ${d.source.x} ${d.source.y} C ${d.source.x} ${(d.source.y + d.target.y) / 2},
147
${d.target.x} ${(d.source.y + d.target.y) / 2}, ${d.target.x} ${d.target.y}`)
148
149
150
const names = svg.append('g')
151
.attr('transform','translate(50,50)')
152
.selectAll('text')
153
.data(root.descendants())
154
.enter()
155
.append('text')
156
.text(d => d.data.child)
157
.attr('x', d => d.x + 8)
158
.attr('y', d => d.y + 2)
159
.style('font-size', '1rem')
160
.on('mouseover', function(e,d){
161
d3.select(this)
162
.transition()
163
.duration('100')
164
.attr('opacity', 1)
165
.style('font-size','2rem')
166
d3.selectAll('text').attr('opacity', '0.3')
167
d3.selectAll('circle').attr('opacity', '0.3')
168
})
169
.on('mouseout', function(d){
170
d3.select(this)
171
.style('font-size','1rem')
172
d3.selectAll('text').attr('opacity', '1')
173
d3.selectAll('circle').attr('opacity', '1')
174
})
JavaScript
1
23
23
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8">
5
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
<script src="https://unpkg.com/d3@7.0.0/dist/d3.min.js"></script>
8
<script src='try.js' defer></script>
9
<style>
10
path {
11
fill:transparent;
12
stroke:teal;
13
}
14
text {
15
cursor: pointer;
16
}
17
</style>
18
<title>Document</title>
19
</head>
20
<body>
21
<svg></svg>
22
</body>
23
</html>
Advertisement
Answer
To highlight the ancestor paths, you need to add id
attributes to the connections
:
JavaScript
1
2
1
.attr('id', d => "link_" + d.target.data._id)
2
And the names
:
JavaScript
1
2
1
.attr('id', d => d.data.child)
2
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:
JavaScript
1
17
17
1
function unhighlightPath(event, d) {
2
//reset all nodes color
3
d3.selectAll("path").style("stroke", "teal");
4
}
5
6
function highlightPath(event, d) {
7
// select link from hovered label to immediate parent
8
d3.select("#link_" + d.data._id).style("stroke", "red");
9
// keep going up until no more parents
10
while (d.parent) {
11
if (d.parent != "null") {
12
d3.selectAll("#link_"+d.parent.data._id).style("stroke", "red")
13
}
14
d = d.parent;
15
}
16
}
17
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.
JavaScript
1
15
15
1
.attr('fill', function(d){
2
//if(d.depth === 0) return 'black'
3
//else if (d.depth === 1) return 'red'
4
//else if (d.depth === 2) return 'green'
5
//else if (d.depth === 3) return 'magenta'
6
//else return 'brown'
7
if (d.children) {
8
// not leaf nodes
9
return "red";
10
} else {
11
// leaf nodes
12
// do some stuff like icons, extra styling
13
return "blue"; // for the node colour
14
}
15
Your code adapted:
JavaScript
1
201
201
1
const familyData = [
2
{
3
_id: '60da7d37b8ca2d2590f0b713',
4
child: 'William',
5
parent: '',
6
parentId: null
7
},
8
{
9
_id: '60da7d7f6a89ad1fecc905e9',
10
child: 'James',
11
parent: 'William',
12
parentId: '60da7d37b8ca2d2590f0b713'
13
},
14
{
15
_id: '60da7d9619156a6d90874aa1',
16
child: 'Henry',
17
parent: 'William',
18
parentId: '60da7d37b8ca2d2590f0b713'
19
},
20
{
21
_id: '60da7db3c1f2f27368395212',
22
child: 'Michael',
23
parent: 'James',
24
parentId: '60da7d7f6a89ad1fecc905e9'
25
},
26
{
27
_id: '60da7dd32796ae5cbc0e1810',
28
child: 'Ethan',
29
parent: 'James',
30
parentId: '60da7d7f6a89ad1fecc905e9'
31
},
32
{
33
_id: '60da7df79f58c4028cb21d06',
34
child: 'Jacob',
35
parent: 'Henry',
36
parentId: '60da7d9619156a6d90874aa1'
37
},
38
{
39
_id: '60da7e149cf24f1d20167c14',
40
child: 'Jack',
41
parent: 'William',
42
parentId: '60da7d37b8ca2d2590f0b713'
43
},
44
{
45
_id: '60da7e2add5413458427c4b2',
46
child: 'Joseph',
47
parent: 'Jack',
48
parentId: '60da7e149cf24f1d20167c14'
49
},
50
{
51
_id: '60da7e48fec03d0b1c2d10d3',
52
child: 'Asher',
53
parent: 'Joseph',
54
parentId: '60da7e2add5413458427c4b2'
55
},
56
{
57
_id: '60da7e5c8cc6f66264b23e70',
58
child: 'Leo',
59
parent: 'Ethan',
60
parentId: '60da7dd32796ae5cbc0e1810'
61
},
62
{
63
_id: '60da7e89cefbdd785cec5ada',
64
child: 'Isaac',
65
parent: 'Leo',
66
parentId: '60da7e5c8cc6f66264b23e70'
67
},
68
{
69
_id: '60da7e93ed9bd0402487e5c8',
70
child: 'Charles',
71
parent: 'Leo',
72
parentId: '60da7e5c8cc6f66264b23e70'
73
},
74
{
75
_id: '60da7ea006b3694914c99ee0',
76
child: 'Caleb',
77
parent: 'Michael',
78
parentId: '60da7db3c1f2f27368395212'
79
},
80
{
81
_id: '60da7eab6a06e223e42b5d65',
82
child: 'Ryan',
83
parent: 'Michael',
84
parentId: '60da7db3c1f2f27368395212'
85
},
86
{
87
_id: '60da7e6b05ff5f0468d8e835',
88
child: 'Thomas',
89
parent: 'Jacob',
90
parentId: '60da7df79f58c4028cb21d06'
91
},
92
{
93
_id: '60da7eb5b7a93714303ef471',
94
child: 'Aaron',
95
parent: 'Thomas',
96
parentId: '60da7e6b05ff5f0468d8e835'
97
},
98
{
99
_id: '60da7ebcf21a2a44503b7596',
100
child: 'Axel',
101
parent: 'Thomas',
102
parentId: '60da7e6b05ff5f0468d8e835'
103
}
104
]
105
106
const dataStructure = d3.stratify()
107
.id(d => d._id)
108
.parentId(d => d.parentId)(familyData)
109
110
const treeStructure = d3.tree().size([500,300])
111
let root = treeStructure(dataStructure)
112
113
//console.log(root.descendants());
114
//console.log(root.links());
115
116
const svg = d3.select('svg')
117
.attr('width',600)
118
.attr('height',600)
119
120
const nodes = svg.append('g')
121
.attr('transform','translate(50,50)')
122
.selectAll('circle')
123
.data(root.descendants())
124
.enter()
125
.append('circle')
126
.attr('cx', d => d.x)
127
.attr('cy', d => d.y)
128
.attr('r', 3)
129
.attr('fill', function(d){
130
//if(d.depth === 0) return 'black'
131
//else if (d.depth === 1) return 'red'
132
//else if (d.depth === 2) return 'green'
133
//else if (d.depth === 3) return 'magenta'
134
//else return 'brown'
135
if (d.children) {
136
// not leaf nodes
137
return "red";
138
} else {
139
// leaf nodes
140
// do some stuff like icons, extra styling
141
return "blue"; // for the node colour
142
}
143
})
144
145
const connections = svg.append('g')
146
.attr('transform','translate(50,50)')
147
.selectAll('path')
148
.data(root.links())
149
.enter()
150
.append('path')
151
.attr('id', d => "link_" + d.target.data._id)
152
.attr('d', d => `M ${d.source.x} ${d.source.y} C ${d.source.x} ${(d.source.y + d.target.y) / 2},
153
${d.target.x} ${(d.source.y + d.target.y) / 2}, ${d.target.x} ${d.target.y}`)
154
155
156
const names = svg.append('g')
157
.attr('transform','translate(50,50)')
158
.selectAll('text')
159
.data(root.descendants())
160
.enter()
161
.append('text')
162
.attr('id', d => d.data.child)
163
.text(d => d.data.child)
164
.attr('x', d => d.x + 8)
165
.attr('y', d => d.y + 2)
166
.style('font-size', '1rem')
167
.on('mouseover', function(e,d){
168
d3.select(this)
169
.transition()
170
.duration('100')
171
.attr('opacity', 1)
172
.style('font-size','2rem')
173
d3.selectAll('text').attr('opacity', '0.3')
174
d3.selectAll('circle').attr('opacity', '0.3');
175
highlightPath(e, d);
176
})
177
.on('mouseout', function(e, d){
178
d3.select(this)
179
.style('font-size','1rem')
180
d3.selectAll('text').attr('opacity', '1')
181
d3.selectAll('circle').attr('opacity', '1');
182
unhighlightPath(e, d)
183
});
184
185
// ancestor paths
186
function unhighlightPath(event, d) {
187
//reset all nodes color
188
d3.selectAll("path").style("stroke", "teal");
189
}
190
191
function highlightPath(event, d) {
192
// select link from hovered label to immediate parent
193
d3.select("#link_" + d.data._id).style("stroke", "red");
194
// keep going up until no more parents
195
while (d.parent) {
196
if (d.parent != "null") {
197
d3.selectAll("#link_"+d.parent.data._id).style("stroke", "red")
198
}
199
d = d.parent;
200
}
201
}
JavaScript
1
7
1
path {
2
fill:transparent;
3
stroke:teal;
4
}
5
text {
6
cursor: pointer;
7
}
JavaScript
1
2
1
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
2
<svg></svg>