Search code examples
d3.jssvgmouseevent

Capturing mouseover events on two overlapping elements


So I have a d3 chart with a rect overlay to hold crosshair elements on mouseover events. Under the overlay I have other rects displaying data that have mouseover event handlers also, but The overlay is blocking mouseover events form triggeron the children rects below.

let chartWindow = svg
  .append("g");

/* this holds axis groups, and cadlestick group*/
let candleStickWindow = chartWindow.append("g")
  //this event never fires
  .on('mousemove', ()=>console.log('mouse move'));

let candlesCrosshairWindow = chartWindow
  .append("rect")
  .attr("class", "overlay")

  .attr("height", innerHeight)
  .attr("width", innerWidth)
  .on("mouseover", function() {
    crosshair.style("display", null);
  })
  .on("mouseout", function() {
    crosshair.style("display", "none");
    removeAllAxisAnnotations();
  })
  .on("mousemove", mousemove);

The CrosshairWindow has CSS property pointer-events: all. If I remove that, I get my events to fire on the candleStickWindow but not the CrosshairWindow. How can I get mouse events onto both of the elements??

Thanks for any help!

Update I changed the crosshair rect element to be on the bottom and it kinda works, the candlestick bars mouseover event works but it blocks the crosshair from working.

enter image description here


Solution

  • One solution that comes to mind might use event bubbling which, however, only works if the events can bubble up along the same DOM sub-tree. If, in your DOM structure, the crosshairs rectangle and the other elements do not share a common ancestor to which you could reasonably attach such listener, you need to either rethink your DOM or resort to some other solution. For this answer I will lay out an alternative approach which is more generally applicable.

    You can position your full-size rect at the very bottom of your SVG and have its pointer-events set to all. That way you can easily attach a mousemove handler to it to control your crosshairs' movements spanning the entire viewport. As you have noticed yourself, however, this does not work if there are elements above which have listeners for that particular event type attached to them. Because in that case, once the event has reached its target, there is no way propagating it further to the underlying rectangle for handling the crosshairs component. The work-around is easy, though, since you can clone the event and dispatch that new one directly to your rectangle.

    Cloning the event is done by using the MouseEvent() constructor passing in the event's details from the d3.event reference:

    new MouseEvent(d3.event.type, d3.event)
    

    You can then dispatch the newly created event object to your crosshairs rect element by using the .dispatchEvent() method of the EventTarget interface which is implemented by SVGRectElement:

    .dispatchEvent(new MouseEvent(d3.event.type, d3.event));
    

    For lack of a full example in your question I set up a working demo myself illustrating the approach. You can drag around the blue circle which is a boiled down version of your crosshairs component. Notice, how the circle can be seamlessly moved around even when under the orange rectangles. To demonstrate the event handlers attached to those small rectangles they will transition to green and back to orange when entering or leaving them with the mouse pointer.

    const width = 500;
    const height = 500;
    const radius = 10;
    const orange = d3.hsl("orange");
    const steelblue = d3.hsl("steelblue");
    const limegreen = d3.hsl("limegreen");
    
    const svg = d3.select("body")
      .append("svg")
        .attr("width", width)
        .attr("height", height);
        
    const target = svg.append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", width)
        .attr("height", height)
        .attr("fill", "none")
        .attr("pointer-events", "all")
        .on("mousemove", () => {
          circle.attr("cx", d3.event.clientX - radius);
          circle.attr("cy", d3.event.clientY - radius);
        });
    
    const circle = svg.append("circle")
        .attr("r", radius)
        .attr("fill", steelblue)
        .attr("pointer-events", "none");
        
    const rect = svg.selectAll(null)
      .data(d3.range(3).map(d => [Math.random() * width, Math.random() * height]))
      .enter().append("rect")
        .attr("x", d => d[0])
        .attr("y", d => d[1])
        .attr("width", 50)
        .attr("height", 50)
        .attr("fill", orange)
        .attr("opacity", 0.5)
        .on("mouseover", function() { 
          d3.select(this).transition().attr("fill", limegreen);
        })
        .on("mousemove", function() { 
          target.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event));
        })
        .on("mouseout", function() { 
          d3.select(this).transition().attr("fill", orange);
        });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>