D3 Circle Pack Layout Mouseover Event Is Getting Triggered Multiple Times
I have this circle pack layout using D3: I have assigned mouseover and mouseout event on the circles in the screenshot, but am not able to figure out why mouseover event is being
Solution 1:
Each inner circle is a <g>
with a <circle>
and <text>
within it. Even though you attach the mouse events to the <g>
, the mouseover
event fires as you pass over the <text>
element in the middle of the circle, causing the tooltip to move as your mouse around.
You can add .attr('pointer-events', 'none')
to the <text>
elements to prevent this:
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none') // <---- HERE
.style('visibility', 'hidden')
.style('fill', 'black');
Example below:
const data = {
name: "root",
children: [
{
name: "A",
children: [
{name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
]
},
{
name: "B",
children: [
{name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
]
},
{
name: "C",
value: 10
},
{
name: "D",
value: 10
},
{
name: "E",
value: 10
}
],
links: [{from: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};
constcloneObj = item => {
if (!item) { return item; } // null, undefined values checklet types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = cloneObj( child );
});
} elseif (typeof item == "object") {
// testing that this is DOMif (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} elseif (!item.prototype) { // check that this is a literalif (item instanceofDate) {
result = newDate(item);
} else {
// it is an object literal
result = {};
for (let i in item) {
result[i] = cloneObj( item[i] );
}
}
} else {
// depending what you would like here,// just keep the reference, or create new objectif (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
constfindNode = (parent, name) => {
if (parent.name === name)
return parent;
if (parent.children) {
for (let child of parent.children) {
const found = findNode(child, name);
if (found) {
return found;
}
}
}
returnnull;
}
constfindNodeAncestors = (parent, name) => {
if (parent.name === name)
return [parent];
const children = parent.children || parent._children;
if (children) {
for (let child of children) {
const found = findNodeAncestors(child, name);
//console.log('FOUND: ', found);if (found) {
return [...found, parent];
}
}
}
returnnull;
}
const svg = d3.select("svg");
// This is for tooltipconstTooltip = d3.select("body").append("div")
.attr("class", "tooltip-menu")
.style("opacity", 0);
constonMouseover = (e,d )=> {
console.log('d -->>', d);
e.stopPropagation();
Tooltip.style("opacity", 1);
let html = `<span>
Hi ${d.data.name}
</span>`;
Tooltip.html(html)
.style("left", (e.pageX + 10) + "px")
.style("top", (e.pageY - 15) + "px");
}
constonMouseout = (e,d ) => {
Tooltip.style("opacity", 0)
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)')
constonClickNode = (e, d) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
if(node.children && !node._children) {
/*node._children = node.children;*/
node._children = cloneObj(node.children);
node.children = undefined;
node.value = 20;
updateGraph(data);
} else {
if (node._children && !node.children) {
//node.children = node._children;
node.children = cloneObj(node._children);
node._children = undefined;
node.value = undefined;
updateGraph(data);
}
}
}
constupdateGraph = graphData => {
constpack = data => d3.pack()
.size([600, 600])
.padding(0)
(d3.hierarchy(data)
.sum(d => d.value * 3.5)
.sort((a, b) => b.value - a.value));
const root = pack(graphData);
const nodes = root.descendants().slice(1);
const nodeElements = container
.selectAll("g.node")
.data(nodes, d => d.data.name);
const addedNodes = nodeElements.enter()
.append("g")
.classed('node', true)
.style('cursor', 'pointer')
.on('click', (e, d) =>onClickNode(e, d))
.on('mouseover',(e, d) =>onMouseover(e, d))
.on('mouseout', (e, d) =>onMouseout(e, d));
addedNodes.append('circle')
.attr('stroke', 'black')
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none')
.style('visibility', 'hidden')
.style('fill', 'black');
const mergedNodes = addedNodes.merge(nodeElements);
mergedNodes
.transition()
.duration(500)
.attr('transform', d =>`translate(${d.x},${d.y})`);
mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.transition()
.duration(1000)
.attr('r', d => d.value)
mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.transition()
.delay(1000)
.style('visibility', 'visible');
const exitedNodes = nodeElements.exit()
exitedNodes.select('circle')
.transition()
.duration(500)
.attr('r', 1);
exitedNodes.select('text')
.remove();
exitedNodes
.transition()
.duration(750)
.remove();
constlinkPath = d => {
let length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
if(length == 0 ) {
return''; // This means its a connection inside collapsed node
}
const fd = d.from.value / length;
const fx = d.from.x + (d.to.x - d.from.x) * fd;
const fy = d.from.y + (d.to.y - d.from.y) * fd;
const td = d.to.value / length;
const tx = d.to.x + (d.from.x - d.to.x) * td;
const ty = d.to.y + (d.from.y - d.to.y) * td;
return`M ${fx},${fy} L ${tx},${ty}`;
};
const links = data.links.map(link => {
letfrom = nodes.find(n => n.data.name === link.from);
if (!from) {
const ancestors = findNodeAncestors(data, link.from);
for (let index = 1; !from && index < ancestors.length -1; index++) {
from = nodes.find(n => n.data.name === ancestors[index].name)
}
}
let to = nodes.find(n => n.data.name === link.to);
if (!to) {
const ancestors = findNodeAncestors(data, link.to);
for (let index = 1; !to && index < ancestors.length -1; index++) {
to = nodes.find(n => n.data.name === ancestors[index].name)
}
}
return {from, to};
});
const linkElements = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
const addedLinks = linkElements.enter()
.append('path')
.classed('link', true)
.attr('marker-end', 'url(#arrowhead-to)')
.attr('marker-start', 'url(#arrowhead-from)');
addedLinks.merge(linkElements)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
linkElements.exit().remove();
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
stroke: blue;
fill: none;
}
div.tooltip-menu {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script><svgwidth="600"height="600"><defs><markerid="arrowhead-to"markerWidth="10"markerHeight="7"refX="10"refY="3.5"orient="auto"><polygonfill="blue"points="0 0, 10 3.5, 0 7" /></marker><markerid="arrowhead-from"markerWidth="10"markerHeight="7"refX="0"refY="3.5"orient="auto"><polygonfill="blue"points="10 0, 0 3.5, 10 7" /></marker></defs></svg>
Post a Comment for "D3 Circle Pack Layout Mouseover Event Is Getting Triggered Multiple Times"