user3871
user3871

Reputation: 12718

Mouseout on element grouping fires before leaving bounding area in D3

I'm trying to add a click and mouseout handler to a group of circles and text. The click handler works fine, but the mouse out seems to fire even if I haven't left the circleGroup area (shown below).

Note: there are multiple of these circle groups which are added in a grid.

enter image description here

Here is the SVG as it appears in the browser:

enter image description here

The code to produce the circleGroup, containing an outer green-ish circle, an inner white circle, and a text element, is as follows:

let circleGroup = leftPanelGroup.append('g');

let outerCircle = circleGroup.append("circle")
    .attr("cx", x)
    .attr("cy", y)
    .style('fill', color)
    .attr("r", 15);

let innerCircle = circleGroup.append("circle")
    .attr("cx", x)
    .attr("cy", y)
    .style('fill', color)
    .style('stroke', '#fff')
    .attr("r", 7);

let text = circleGroup.append('text')
    .style('color', '#fff')
    .attr("x", x)
    .attr("y", y - 25)
    .style('fill', '#fff')
    .attr("font-size", 12)
    .attr('font-weight', 'bold')
    .attr("font-family", "sans-serif")
    .attr('id', 'circle-text')
    .style("text-anchor", "middle")
    .text('Thank you');

...

On click anywhere within the circleGroup, the circleClick should fire. This works fine. The issue is, the circleMouseout function seems to randomly fire even if I haven't yet left the bounding area of the circleGroup:

circleGroup.on('click', circleClick).on('mouseout', circleMouseout);

function circleClick() {
    // Do something
}
function circleMouseout() {
    // Do something else
}

The output HTML in console shows the area of the <g> svg group element. I'd expect that click anywhere in this highlighted area would fire the click event, and only when I mouse out of this highlighted area would the mouseout event fire. Again, the click works fine, the mouseout does not.

enter image description here

<g>
   <circle cx="252.99037499999997" cy="340.938" r="15" style="fill: rgb(108, 160, 123);"> 
   </circle>
   <circle cx="252.99037499999997" cy="340.938" r="7" style="fill: rgb(108, 160, 123); stroke: rgb(255, 255, 255);">
   </circle>
   <text x="252.99037499999997" y="315.938" font-size="12" font-weight="bold" font-family="sans-serif" id="circle-text" style="color: rgb(255, 255, 255); fill: rgb(255, 255, 255); text-anchor: middle;">Thank you
   </text>
</g>

Upvotes: 1

Views: 140

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38161

The bounding box of a g doesn't affect where a mouse interacts with it.The mouse only interacts with the "interaction area" of an element. This is generally the stroke or fill of rendered elements. So, whenever your mouse leaves the circles or the text, you trigger the mouseout event, not when you leave the bounding box of the parent g.

If you want the bounding box of the parent g to interact with the mouse, then we need to add a new rectangle. We can extract the g's bbox and use this to draw a new rectangle over the bounding box of the g. This rectangle can be given the fill of none to make it invisible, but also given a pointer-events property of all to ensure it interacts with the mouse:

let boundingBox = circleGroup.append('rect')
  .each(function() {
     var bbox = this.parentNode.getBBox();  // get parent `g` bounding box
     d3.select(this)
       .attr("width", bbox.width)           // size rect based on bounding box.
       .attr("height", bbox.height)
       .attr("x", bbox.x)
       .attr("y", bbox.y)
       .attr("fill","none")
       .attr("pointer-events","all")
  })

Now we can assign event listeners to the g, or the rect for that matter, and the entire g bounding box will respond to mouse events:

let circleGroup = d3.select("body")
  .append("svg")
  .append("g");

let x = 50;
let y = 50;
let color = "steelblue";

let outerCircle = circleGroup.append("circle")
    .attr("cx", x)
    .attr("cy", y)
    .style('fill', color)
    .attr("r", 15);

let innerCircle = circleGroup.append("circle")
    .attr("cx", x)
    .attr("cy", y)
    .style('fill', color)
    .style('stroke', '#fff')
    .attr("r", 7);

let text = circleGroup.append('text')
    .attr("x", x)
    .attr("y", y - 25)
    .style('fill', color)
    .attr("font-size", 12)
    .attr('font-weight', 'bold')
    .attr("font-family", "sans-serif")
    .attr('id', 'circle-text')
    .style("text-anchor", "middle")
    .text('Thank you');

let boundingBox = circleGroup.append('rect')
  .each(function() {
     var bbox = this.parentNode.getBBox();
     d3.select(this)
       .attr("width", bbox.width)
       .attr("height", bbox.height)
       .attr("x", bbox.x)
       .attr("y", bbox.y)
       .attr("fill","none")
       .attr("pointer-events","all")
  })

circleGroup.on("mouseover", function() {
     console.log("mouseover");
  }).on("mouseout", function() {
     console.log("mouseout");
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Upvotes: 2

Related Questions