Detecting Mouse Events on Multiple Overlapping SVG Elements
Asked Answered
H

2

10

I'm trying to detect mousemove events on partially overlapping SVG elements, as in this image

enter image description here

fiddle

<svg>
    <rect id="red"    x=10 y=10 width=60 height=60 style="fill:#ff0000" />
    <rect id="orange" x=80 y=10 width=60 height=60 style="fill:#ffcc00" />
    <rect id="blue"   x=50 y=30 width=60 height=60 style="fill:#0000ff; fill-opacity: 0.8" />
</svg>

$('rect').on('mousemove', function()
{
    log(this.id);
});

Now, when hovering the mouse over the blue/red intersection I'd like to detect mouse events on both those elements, and the same for the blue/orange combo. As you can see in the logs, in those cases the event is currently only fired for the blue box as it is on top.

This has to do with pointer-events, as I can get the red and orange elements to fire the event while hovering the blue element by setting the blue element's pointer-events to none. But then I don't get the events for the blue box, so that is not a viable option either.

I will use whichever library solves this problem. I looked at event bubbling like in this d3 example, but that only works for elements that are nested in the DOM. I have lots of independent elements that may overlap with lots of other elements and can therefore not structure my DOM that way.

I'm guessing the last resort is to find the elements that are at the current mouse position, and manually firing the events. Therefore, I looked at document.elementFromPoint(), but that would only yield 1 element (and may not work in SVG?). I found the jQuerypp function within, that finds the elements at a given position, see here. That example looks great, except it's DIVs and not inside SVG. When replacing divs with svg rectangle elements, the fiddle seems to break.

What do I do?!

Haney answered 31/3, 2015 at 19:57 Comment(9)
It looks like jquerypp is using offsetWidth and offsetHeight which (in chrome at least) return 0s for the rects.Ruyle
Can you count on each <rect> having both width and height attributes? If so, you can call parseInt(el.attr("width")) to use the width as an integer. See if this fiddle would work for you.Chung
@JamesMontagne Good point. That can probably be fixed, then it would work. Gonna look into that.Haney
@AustinMullins Thanks, that's the idea of the within() function, slightly different implementation. In my case I would have to detect whether the mouse is within circles, so I could indeed take advantage of the simplicity of the shapes. Would be nice to not rely on that so this works with things like curved paths too, but I'll definitely take that if I have to.Haney
This might be helpful. In particular, have a look at the linked working example: https://mcmap.net/q/517359/-hit-testing-svg-shapesRuyle
@JamesMontagne Wow, that's great! Haven't yet checked which browsers support svg.getIntersectionList(), but that looks like a great option!Haney
@JamesMontagne working fiddle with getIntersectionList: jsfiddle.net/michaschwab/w0wufbtn/5Haney
@JamesMontagne You can post that as answer if you like. Thanks to both of you!Haney
@MichaSchwab That's alright, since you have a working example, I would suggest you post an answer with that fiddle and accept that for future users.Ruyle
H
7

The great comments here gave me the answer: It's possible to propagate the event to underlying elements manually by finding them using getIntersectionList() at the cursor positon.

$('svg').on('mousemove', function(evt)
{
    var root = $('svg')[0];
    var rpos = root.createSVGRect();
    rpos.x = evt.clientX;
    rpos.y = evt.clientY;
    rpos.width = rpos.height = 1;
    var list = root.getIntersectionList(rpos, null);

    for(var i = 0; i < list.length; i++)
    {
        if(list[i] != evt.target)
        {
            $(list[i]).mousemove();
        }
    }
});

Working example: http://jsfiddle.net/michaschwab/w0wufbtn/6/

If the other listeners need the original event object, check out http://jsfiddle.net/michaschwab/w0wufbtn/13/.

Thanks a lot!!

Haney answered 1/4, 2015 at 19:14 Comment(8)
This works fine in IE, Chrome and Opera (and probably Safari), but note that getIntersectionList isn't implemented in Firefox yet, see bugzilla.mozilla.org/show_bug.cgi?id=501421.Twit
Also your invoked mousemove doesn't have info about the original event, so for example the orange rect doesn't receive info about the mouse coords. Passing in mousemove(evt) doesn't solve that either.Runway
Thanks for the comments. @thund: This has to do with jQuery, and can be resolved by calling trigger() instead of mousemove(). See jsfiddle.net/michaschwab/w0wufbtn/12 for the updated answer.Haney
Nice, that seems to fix it. But I notice that covered just keeps growing (which you can see with log(covered.length);, so would eventually lead to memory problems.Runway
I mean sure, it was just an example. But replacing covered with just a single lastEvent works also and fixes that problem. jsfiddle.net/michaschwab/w0wufbtn/13Haney
What if my SVG is below a canvas, and I want to detect mouse event on both of the overlapped elements?Franckot
@Jiayang: How about this? jsfiddle.net/michaschwab/w0wufbtn/112Haney
For anyone stumbling upon this and looking for a cross-browser solution that includes Firefox (though possibly not very performant), see my answer over at this question: #55907322Octopod
S
2

For anyone still looking, elementsFromPoint() returns a node list of all the elements under your mouse cursor.

NOTE: there is also a elementFromPoint() method.

This is particularly useful when you need to detect multiple overlapping SVG path elements on mouseover.

A simple example:

Get the nodeList from your mouse event.

const _overlapped = document.elementsFromPoint(e.pageX, e.pageY)

Filter the list based on some criterion:

// Some list of element id's you're interested in
const _lines = ['elId1', 'elId2', 'elId3'] 

// Check to see if any element id matches an id in _lines   
const _included = _overlapped.filter(el => _lines.includes(el.id))

// Perform an action on each member in the list
_included.forEach(...) 
Sokil answered 25/2, 2022 at 15:58 Comment(1)
Thank you for sharing this function! I've never seen it before and I have been developing for years, this just helped me solve someone else's problem. Cheers!Decontaminate

© 2022 - 2024 — McMap. All rights reserved.