9

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?!

Community
  • 1
  • 1
Micha Schwab
  • 786
  • 1
  • 6
  • 21
  • 2
    It looks like jquerypp is using `offsetWidth` and `offsetHeight` which (in chrome at least) return 0s for the rects. – James Montagne Mar 31 '15 at 20:11
  • 1
    Can you count on each `` 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](http://jsfiddle.net/amullins/5c88b8yf/) would work for you. – Austin Mullins Mar 31 '15 at 20:20
  • @JamesMontagne Good point. That can probably be fixed, then it would work. Gonna look into that. – Micha Schwab Mar 31 '15 at 20:22
  • 1
    @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. – Micha Schwab Mar 31 '15 at 20:26
  • 1
    This might be helpful. In particular, have a look at the linked working example: http://stackoverflow.com/a/2178680/717383 – James Montagne Mar 31 '15 at 20:39
  • @JamesMontagne Wow, that's great! Haven't yet checked which browsers support svg.getIntersectionList(), but that looks like a great option! – Micha Schwab Mar 31 '15 at 20:49
  • @JamesMontagne working fiddle with getIntersectionList: http://jsfiddle.net/michaschwab/w0wufbtn/5/ – Micha Schwab Mar 31 '15 at 21:00
  • @JamesMontagne You can post that as answer if you like. Thanks to both of you! – Micha Schwab Mar 31 '15 at 21:31
  • @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. – James Montagne Apr 01 '15 at 02:36

2 Answers2

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

Micha Schwab
  • 786
  • 1
  • 6
  • 21
  • 2
    This works fine in IE, Chrome and Opera (and probably Safari), but note that getIntersectionList isn't implemented in Firefox yet, see https://bugzilla.mozilla.org/show_bug.cgi?id=501421. – Erik Dahlström Apr 02 '15 at 08:25
  • 1
    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. – thund Sep 02 '15 at 19:57
  • Thanks for the comments. @thund: This has to do with jQuery, and can be resolved by calling `trigger()` instead of `mousemove()`. See http://jsfiddle.net/michaschwab/w0wufbtn/12/ for the updated answer. – Micha Schwab Sep 03 '15 at 20:42
  • 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. – thund Sep 04 '15 at 00:31
  • I mean sure, it was just an example. But replacing `covered` with just a single `lastEvent` works also and fixes that problem. http://jsfiddle.net/michaschwab/w0wufbtn/13/ – Micha Schwab Sep 11 '15 at 17:08
  • What if my SVG is below a canvas, and I want to detect mouse event on both of the overlapped elements? – Jiayang Nov 28 '17 at 00:03
  • @Jiayang: How about this? http://jsfiddle.net/michaschwab/w0wufbtn/112/ – Micha Schwab Mar 25 '18 at 17:39
  • 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: https://stackoverflow.com/questions/55906822/how-to-make-an-hover-effect-on-two-overlapping-shapes-in-svg?noredirect=1#answer-55907287 – Constantin Groß Apr 29 '19 at 16:23
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(...) 
Vladimir Mujakovic
  • 650
  • 1
  • 7
  • 21
  • 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! – Aaron Meese Aug 11 '22 at 12:52