Cecilia
Cecilia

Reputation: 4721

Propagating events through overlapping svg elements

I have two overlapping svg.g groups with different onclick events. I periodically blend the groups in or out of the visualization using the opacity attribute. Currently, only the onclick event of the group that is rendered on top is called, but I would like to call the event for the group that is currently visible. Alternatively, I could always call both events and use a conditional statement inside the called function which depended on the opacity attribute.

Here is an example

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

</head>
<body>
    <div id="body"></div>
    <script type="text/javascript">

    var canvas_w = 1280 - 80,
        canvas_h = 800 - 180;

    var svg = d3.select("#body").append("div")
        .append("svg:svg")
        .attr("width", canvas_w)
        .attr("height", canvas_h)   

    var visible_group = svg.append("g")
        .attr("opacity", 1)
        .on("click", function(d){console.log("Click")})
        .append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 100)
        .attr("height", 100)
        .style("fill", "blue");

    var invisible_group = svg.append("g")
        .attr("opacity", 0)
        .on("click", function(d){console.log("Invisiclick")})
        .append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 100)
        .attr("height", 100)
        .style("fill", "red");


    </script>
</body>
</html>

This code will render a blue rectangle, the visible group. The group with the red rectangle is hidden. If you click on the blue rectangle, "Invisiclick" will be printed to the console, the onclick event of the hidden group. I would like to print "Click" to the console, or alternatively, both "Invisiclick" and "Click".

How can I do this?

Upvotes: 2

Views: 2355

Answers (2)

veproza
veproza

Reputation: 2994

Opacity does make the elements translucent, it doesn't make them disappear. Just as you can tap a piece of glass, you can click an element with opacity:0.

Now, there are two options, based on whether the shapes are different in both views. If they aren't (say, you're drawing a world map, the countries stay the same, just the color changes), it might be easiest to listen to the topmost layer and then run an if-statement which part to execute. Like this

    var state = "blue";


    var clickHandler = function() {
        if(state === "blue") {
            console.log("Blue clicked");
        } else {
            console.log("Red clicked");
        }
    }


    var toggleState = function() {
        state = (state === "blue") ? "red" : "blue";
    }

    var updateDisplay = function() {
        blueGroup
            .transition()
            .duration(400)
            .attr("opacity", state === "blue" ? 1 : 0);
        redGroup
            .transition()
            .duration(400)
            .attr("opacity", state === "red" ? 1 : 0);
    }

    var canvas_w = 1280 - 80,
        canvas_h = 120;

    var svg = d3.select("#body").append("div")
        .append("svg:svg")
        .attr("width", canvas_w)
        .attr("height", canvas_h)   

    var blueGroup = svg.append("g")
        .append("rect")
        .attr("opacity", 1)
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 100)
        .attr("height", 100)
        .style("fill", "blue");

    var redGroup = svg.append("g")
        .on("click", clickHandler)
        .append("rect")
        .attr("opacity", 0)
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 100)
        .attr("height", 100)
        .style("fill", "red");



    d3.select("button").on("click", function() {
        toggleState();
        updateDisplay();
    });
<script src="https://samizdat.cz/tools/d3/3.5.3.min.js" charset="utf-8"></script>
<div id="body"></div>
<button>change!</button>

If on the other hand the shapes change, you will need to first make the elements translucent with opacity:0 and then make them disappear with display:none (otherwise, they will flash out instantly). An alternative is pointer-events, but only if you don't need to support old browsers. The transition would then look like this:

var state = "blue";

var toggleState = function() {
    state = (state === "blue") ? "red" : "blue";
}

var updateDisplay = function() {
    blueGroup
        .style("display", state === "blue" ? "block" : "none")
        .transition()
        .duration(400)
        .attr("opacity", state === "blue" ? 1 : 0)
        .each("end", function() {
            blueGroup.style("display", state === "blue" ? "block" : "none");
        });
        
    redGroup
        .style("display", state === "red" ? "block" : "none")
        .transition()
        .duration(400)
        .attr("opacity", state === "red" ? 1 : 0)
        .each("end", function() {
            redGroup.style("display", state === "red" ? "block" : "none");
        });
}

var canvas_w = 1280 - 80,
    canvas_h = 120;

var svg = d3.select("#body").append("div")
    .append("svg:svg")
    .attr("width", canvas_w)
    .attr("height", canvas_h)   

var blueGroup = svg.append("g")
    .on("click", function() {
        console.log("Blue clicked");
    })
    .append("rect")
    .attr("opacity", 1)
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 150)
    .attr("height", 100)
    .style("fill", "blue");

var redGroup = svg.append("g")
    .on("click", function() {
        console.log("Red clicked");
    })
    .append("rect")
    .attr("opacity", 0)
    .style("display", "none")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 100)
    .attr("height", 120)
    .style("fill", "red");



d3.select("button").on("click", function() {
    toggleState();
    updateDisplay();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="body"></div>
<button>change!</button>

Note that on each transition, we now have to handle both opacity and display, and in correct order. Also note that now we have listeners on both rects.

The example would be quite a bit simpler if it could be used with .enter() and .exit() selections, as you could make away with the .on("end") and instead use .remove() on the exiting transitions.

Update: practically identical to display:none is also visibility: hidden.

Upvotes: 3

Cecilia
Cecilia

Reputation: 4721

If you use the style visibility rather than the attribute opacity to set the groups as hidden or visible, you can also use the style pointer-events to restrict events to visible elements.

var canvas_w = 1280 - 80,
    canvas_h = 800 - 180;

var svg = d3.select("#body").append("div")
    .append("svg:svg")
    .attr("width", canvas_w)
    .attr("height", canvas_h)   

var visible_group = svg.append("g")
    .style("visibility", "visible")
    .style("pointer-events", "visible")
    .on("click", function(d){console.log("Click")})
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 100)
    .attr("height", 100)
    .style("fill", "blue");

var invisible_group = svg.append("g")
    .style("visibility", "hidden")
    .style("pointer-events", "visible")
    .on("click", function(d){console.log("Invisiclick")})
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", 100)
    .attr("height", 100)
    .style("fill", "red");


</script>

This example will print "Click" to the console when you click the blue rectangle.

Upvotes: 2

Related Questions