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?
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.
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.
Related
I have a button that is a rectangle and text and an invisible rectangle on top. The invisible rectangle helps make sure that a user can click anywhere on the button, including over the word PLAY.
The invisible rectangle is preventing my .on("mouseover") effect from working, which changes the color of the button.
This is my code -
// svg element
var ticker = d3.select("#ticker").select("svg")
.attr("width", 300)
.attr("height", 50);
// group for button elements
ticker = d3.select("#ticker").select("svg")
.attr("class", "ticker")
.attr("transform", "translate(0,0)")
.append("g")
// button with color
ticker.append("rect")
.attr("class", "pauseplay")
.attr("x", "120")
.attr("y", "0")
.attr("height","30")
.attr("width","100")
.attr("rx","5")
.style("fill","#FF5960")
.on("mouseover", function() { d3.select(this).style("fill", "#ff7b7b"); })
.on("mouseout", function() { d3.select(this).style("fill", "#FF5960"); })
// button text
ticker.append("text")
.attr("class","btn-text")
.attr("x","150")
.attr("y","20")
.text("PAUSE")
.style("fill","white")
// invisible button
ticker.append("rect")
.attr("class","play")
.attr("x", "120")
.attr("y", "0")
.attr("height","30")
.attr("width","100")
.style("opacity","0")
.on("click", function() {
PAUSED = !PAUSED;
t.stop();
// Play it.
if (!PAUSED) {
ticker.selectAll(".btn-text").text("PAUSE")
timer(); }
// Pause it.
else {
ticker.selectAll(".btn-text").text("PLAY")
} })
I'd like for the user to be able to hover anywhere, but also have the color change on mouseover.
You're bending over backwards, you don't need that invisible rectangle. Just set the text's pointer-events to none and add the event listener to the visible rectangle.
Here is your code with that change:
// svg element
var ticker = d3.select("#ticker").select("svg")
.attr("width", 300)
.attr("height", 50);
// group for button elements
ticker = d3.select("#ticker").select("svg")
.attr("class", "ticker")
.attr("transform", "translate(0,0)")
.append("g")
// button with color
ticker.append("rect")
.attr("class", "pauseplay")
.attr("x", "120")
.attr("y", "0")
.attr("height", "30")
.attr("width", "100")
.attr("rx", "5")
.style("fill", "#FF5960")
.on("mouseover", function() {
d3.select(this).style("fill", "#ff7b7b");
})
.on("mouseout", function() {
d3.select(this).style("fill", "#FF5960");
})
.on("click", function() {
console.log("click")
})
// button text
ticker.append("text")
.attr("class", "btn-text")
.attr("x", "150")
.attr("y", "20")
.text("PAUSE")
.style("fill", "white")
.attr("pointer-events", "none");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="ticker">
<svg></svg>
</div>
I think the easiest way would be to create a hover function trigerred by the invisible button (you have to add IDs) like for example :
ticker.append("rect")
.attr("class", "pauseplay")
.attr("id","myID")
.attr("x", "120")
.attr("y", "0")
.attr("height","30")
.attr("width","100")
.attr("rx","5")
.style("fill","#FF5960")
.on("mouseout", function() { d3.select(this).style("fill", "#FF5960"); })
// invisible button
ticker.append("rect")
.attr("class","play")
.attr("x", "120")
.attr("y", "0")
.attr("height","30")
.attr("width","100")
.style("opacity","0")
.on("mouseover", function() { d3.select("#myID").style("fill", "#ff7b7b"); })
.on("click", function() {
PAUSED = !PAUSED;
t.stop();
// Play it.
if (!PAUSED) {
ticker.selectAll(".btn-text").text("PAUSE")
timer(); }
// Pause it.
else {
ticker.selectAll(".btn-text").text("PLAY")
} })
I'm trying to get a simple mouseover to work on some rectangles in d3. If I append the SVG to "body" everything works just fine (first line of code). If I change the select to ".chart1" instead of "body" the mouseovers don't work. Has anyone seen this before?
var chart = d3.select(".chart1").append("svg")
.attr("width", 250)
.attr("height", 50);
data = ["a", "b", "c",]
for(var i = 0; i < data.length; i++){
var rect = chart.append("rect")
.attr("x", 20*i)
.attr("y", 10)
.attr("width", 15)
.attr("height", 15)
chart.selectAll('rect')
.on("mouseover", function() {
d3.select(this)
.attr("opacity", .5)
})
.on("mouseout", function() {
d3.select(this)
.attr("opacity", 1)
});
jsfiddle: https://jsfiddle.net/jasonleehodges/um5f5ysv/
The problem is not because you are appending svg to body or div with class .chart1
The problem of mouseover not working is in here.
var chart = d3.select(".chart1").append("svg")
.attr("width", 250)
.attr("height", 50)
//.style("pointer-events", "none");//WHY DISABLE MOUSE EVENT ON SVG
Fix would be to remove .style("pointer-events", "none");
and it will work on all the cases without any anomaly.
Working code here
On another note you should not use a for loop, well that's not the d3 way(as put by #Gerardo Furtado).
so your code with for:
for(var i = 0; i < data.length; i++){
var rect = chart.append("rect")
.attr("x", 20*i)
.attr("y", 10)
.attr("width", 15)
.attr("height", 15)
instead should be
var rect = chart.selectAll("rect").data(data).enter().append("rect")
.attr("x", function(d,i){return 20*i})
.attr("y", 10)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "#cc004c")
.attr("title","NBC")
.attr("data-toggle","tooltip")
working code here
I am interested in learning how to create a transparent mask with d3.js.
http://jsfiddle.net/59bunh8u/35/
This is where I am up to - how would I create a subtraction mask on the red rectangle - also how could you style the red rectangle to take on more of a multiply style property?
$(document).ready(function() {
var el = $(".mask"); //selector
// Set the main elements for the series chart
var svg = d3.select(el[0]).append("svg")
.attr("class", "series")
.attr("width", "800px")
.attr("height", "500px")
.append("g")
.attr("transform", "translate(0,0)")
var rect = svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.attr("height", 500)
.style("fill", "red")
.style('opacity', 0.75)
var rect = svg
.append("circle").attr("cx", 250).attr("cy", 250).attr("r", 125).style("fill", "white");
});
You need an SVG mask. Feel free to play with it to tweak the parameters:
var mask = svgroot
.append("defs")
.append("mask")
.attr("id", "myMask");
mask.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 500)
.attr("height", 500)
.style("fill", "white")
.style("opacity", 0.7);
mask.append("circle")
.attr("cx", 300)
.attr("cy", 300)
.attr("r", 100);
Modified example: http://jsfiddle.net/59bunh8u/40/
See also SVG clipPath to clip the *outer* content out
I would like to prevent the mouseout action of an element when the user click on this element. For an example see this JSFiddle (the circle disappear even if I click on the label).
Is there an easy way to achieve my objective with d3.js? Thank you!
The JSFiddle example code:
var svg = d3.select("#content")
.append("svg")
.attr("width", 600)
.attr("height", 400);
var g = svg.append("g");
var text = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 50)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var circle = g.append("circle")
.style("fill", "Orange")
.attr("cx", 150)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass", true);
function mouseover(p){
d3.selectAll("circle.someClass").classed("hide", false);
}
function mouseout(p){
d3.selectAll("circle.someClass").classed("hide", true);
}
function click(p){
d3.selectAll("circle.someClass").classed("hide", false);
}
If you plan to only have one element that controls the circle, use a "flag". Messing with conditionally registering/unregistering events is not a good idea.
Check this updated version of your fiddle:
https://jsfiddle.net/cze9rqf7/
var svg = d3.select("#content")
.append("svg")
.attr("width", 600)
.attr("height", 400);
var g = svg.append("g");
var text = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 50)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var circle = g.append("circle")
.style("fill", "Orange")
.attr("cx", 150)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass", true);
var isClicked = false;
function mouseover(p){
d3.selectAll("circle.someClass").classed("hide", false);
}
function mouseout(p){
if(!isClicked) {
d3.selectAll("circle.someClass").classed("hide", true);
}
}
function click(p){
isClicked = !isClicked;
d3.selectAll("circle.someClass").classed("hide", false);
}
EDITS For Comments
If you need to "remember" state per element, instead of using a global, you should be using data-binding on those elements:
var text = g.append("text")
.datum({isClicked: false})
.text("Click me")
...
function mouseout(p){
// p is the data-bound object
if(!p.isClicked) {
var className = d3.select(this).attr("class");
d3.selectAll("circle."+className).classed("hide", true);
}
}
function click(p){
// on click edit the data-bound object
p.isClicked = !p.isClicked;
var className = d3.select(this).attr("class");
d3.selectAll("circle."+className).classed("hide", false);
}
Updated fiddle here.
Here is an answer without any "flag":
Given that you have many labels, one option is removing the mouseout for the clicked element:
d3.select(this).on("mouseout", null);
Here is your updated fiddle: https://jsfiddle.net/gerardofurtado/38p18pLt/
And the same code in the Stack snippet:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 400);
var g = svg.append("g");
var text = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 50)
.classed("someClass", true)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var text2 = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 150)
.classed("someClass2", true)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var circle = g.append("circle")
.style("fill", "Orange")
.attr("cx", 150)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass", true);
var circle2 = g.append("circle")
.style("fill", "Green")
.attr("cx", 250)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass2", true);
function mouseover(p) {
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", false);
}
function mouseout(p) {
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", true);
}
function click(p) {
d3.select(this).on("mouseout", null);
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", false);
}
.hide {
display: none;
}
text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You can also toggle the click, making the mouseout work again:
if(d3.select(this)[0][0].__onmouseout){
d3.select(this).on("mouseout", null);
} else {
d3.select(this).on("mouseout", mouseout);
}
Here is the fiddle with the "toggle" function: https://jsfiddle.net/gerardofurtado/4zb9gL9r/1/
And the same code in the Stack snippet:
var svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 400);
var g = svg.append("g");
var text = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 50)
.classed("someClass", true)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var text2 = g.append("text")
.text("Click me")
.style("fill", "Blue")
.attr("x", 50)
.attr("y", 150)
.classed("someClass2", true)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click);
var circle = g.append("circle")
.style("fill", "Orange")
.attr("cx", 150)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass", true);
var circle2 = g.append("circle")
.style("fill", "Green")
.attr("cx", 250)
.attr("cy", 90)
.attr("r", 15)
.classed("hide", true)
.classed("someClass2", true);
function mouseover(p) {
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", false);
}
function mouseout(p) {
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", true);
}
function click(p) {
if (d3.select(this)[0][0].__onmouseout) {
d3.select(this).on("mouseout", null);
} else {
d3.select(this).on("mouseout", mouseout);
}
var className = d3.select(this).attr("class");
d3.selectAll("circle." + className).classed("hide", false);
}
.hide {
display: none;
}
text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am not sure what's going on, but I have 2 very simple examples set up to show what I am asking.
Both examples have a 'g' that contains a 'rect' and 'text'.
In the 1st example, I am setting up drag on the 'g' itself, i.e., if you mousedown anywhere in that group and drag, it will drag the entire thing (both 'rect' and 'text') around the viewpoint.
http://jsfiddle.net/wup4d0nx/
var chart = d3.select("body").append("svg")
.attr("height", 500)
.attr("width", 500)
.style("background", "lightgrey");
var group = chart.selectAll("g")
.data(["Hello"])
.enter()
.append("g")
.attr("id", function (d) { return d;});
var rect = group.append("rect")
.attr("stroke", "red")
.attr("fill", "blue")
.attr("width", 200)
.attr("height", 200)
.attr("x", 10)
.attr("y", 10);
var label = group.append("text")
.attr("x", 40)
.attr("y", 40)
.attr("font-size", "22px")
.attr("text-anchor", "start")
.text(function (d) { return d;});
// Set up dragging for the entire group
var dragMove = function (d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
};
var drag = d3.behavior.drag()
.origin(function (data) {
var element = d3.select("#" + data);
return {
x: d3.transform(element.attr("transform")).translate[0],
y: d3.transform(element.attr("transform")).translate[1]
};
})
.on("drag", dragMove);
group.call(drag);
In the 2nd example, which doesn't work and is what I am interested in, I want ONLY THE TEXT to be something the user can grab to drag the entire group around.
I tried many attempts. Some don't work at all, some work but flicker like the example I provide here:
http://jsfiddle.net/9xeo7ehf/
var chart = d3.select("body").append("svg")
.attr("height", 500)
.attr("width", 500)
.style("background", "lightgrey");
var group = chart.selectAll("g")
.data(["Hello"])
.enter()
.append("g")
.attr("id", function (d) { return d;});
var rect = group.append("rect")
.attr("stroke", "red")
.attr("fill", "blue")
.attr("width", 200)
.attr("height", 200)
.attr("x", 10)
.attr("y", 10);
var label = group.append("text")
.attr("x", 40)
.attr("y", 40)
.attr("font-size", "22px")
.attr("text-anchor", "start")
.text(function (d) { return d;});
// Set up dragging for the entire group USING THE LABEL ONLY TO DRAG
var dragMove = function (d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this.parentNode).attr("transform", "translate(" + x + "," + y + ")");
};
var drag = d3.behavior.drag()
.origin(function (data) {
var element = d3.select("#" + data);
return {
x: d3.transform(element.attr("transform")).translate[0],
y: d3.transform(element.attr("transform")).translate[1]
};
})
.on("drag", dragMove);
label.call(drag);
What's going on with this that it flickers and what am I doing wrong?
Any help would be greatly appreciated!
Thanks!
I'm not sure exactly why it is flickering (as I am not too familiar with D3), but one way to get it to stop is to use the source event for D3:
// 50 is the offset x/y position you set for your text
var x = d3.event.sourceEvent.pageX - 50;
var y = d3.event.sourceEvent.pageY - 50;
Edit: While the above code works, it causes the box to initially "jump" to the coordinates of the text, A better fix would be to take your first example and just filter our events that aren't executed on the text element. Try putting the following at the top of the dragMove method:
if(d3.event.sourceEvent.target.nodeName !== 'text') {
return;
}
Try d3.event.sourceEvent.stopPropagation(); inside on-drag function