How can I add a d3-tip / mouseover event AFTER a transition on a histogram / bar chart?
I create a bar chart / plot:
canvas.call(tip);
var sampleBars = canvas.selectAll(".sampleBar")
.data(data)
.enter().insert("rect", ".axis")
.attr("class", "sampleBar")
.attr("x", function(d) { return x(d.x) + 1; })
.attr("y", function(d) { return y(d.y); })
.attr("width", x(data[0].dx + data[0].x) - x(data[0].x) - 1)
.attr("height", 0)
.transition()
.duration(2500)
.delay(500)
.attr("height", function(d) { return height - y(d.y); });
I want to add:
sampleBars
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
And this is the tip:
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<span style='color:white'>" + d3.round((d.y * 100))
+ "%" + "</span>" + " infected"; })
So that the mouseover event occurs after the transition is complete. But I get the error:
Uncaught TypeError: undefined is not a function
The error is for the line:
.on('mouseover', tip.show)
I think there is a simple flaw in my logic. I seem to be confused about how these events or attributes interact with each other.
Again: I want to 'activate' the mouseover tip AFTER the transition is complete so that after the transition does its thing the tip will appear if the user puts their mouse over each bar. I have no problem creating the mouseover event and having it work on user mouseover to display the data I want, but I am having no luck with making this work with a transition of those bars.
Instead of adding/removing events, one approach is to simply set/unset the pointer-events attribute so that the events don't fire when you want them suppressed.
var myselection = d3.select('body')
.append('svg').attr({height: 200, width: 200})
.append('circle')
.attr( {cx: 100, cy: 100, r: 0})
.attr('pointer-events', 'none')
.on('mouseover', function() { console.log('mouseover'); })
.on('mouseout', function() { console.log('mouseout'); })
myselection
.transition().duration(4000).attr('r', 100)
.transition().attr('pointer-events', 'auto')
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you have the console window open you'll notice that mouse over/out logs nothing until the circle stops growing.
Related
I have made a map with D3 and using some data from nasa.gov(https://data.nasa.gov/resource/y77d-th95.geojson)
Here is the codepen
http://codepen.io/redixhumayun/full/VPepqM/
I have tried making a tooltip with the following code.
//setting up the tooltip here
var div = svg.append('div')
.attr('class', 'tooltip')
.style('opacity', 0.7);
var meteorites = meteorite.selectAll('circle')
.data(data.features)
.enter()
.append('circle')
.attr('cx', function(d) {
return projection([d.properties.reclong, d.properties.reclat])[0]
})
.attr('cy', function(d) {
return projection([d.properties.reclong, d.properties.reclat])[1]
})
.attr('fill', function(d) {
return color_scale(d.properties.mass)
})
.attr('stroke', 'black')
.attr("stroke-width", 1)
.attr('r', function(d) {
return weight_scale(d.properties.mass);
})
.attr('fill-opacity', function(d) {
if (weight_scale(d.properties.mass) > 7) {
return 0.5
}
return 1;
})
.on('mouseover', function(d) {
div.transition().duration(200)
.style('opacity', 0.9)
.style('left', (d3.event.pageX) + 'px')
.style('top', (d3.event.pageY / 1.5) + 'px')
div.html('<p>Please show up</p>');
}).on('mouseout', function(d){
div.transition().duration(200)
.style('opacity', 0);
})
However, the tooltip does not show up. I even tried changing the z-index of the tooltip to be greater than that of the underlying map so that it wouldn't be hidden by the map, but no luck.
When I inspect the tooltip in the elements inspector, it shows that the style, left and top attributes of the tooltip div are changing, but I can't seem to see it on the screen. Not sure what I'm doing wrong here.
You have three problems here:
First, set the position of the <div> to absolute in the CSS:
position: absolute;
Second, the biggest problem: you cannot append a <div> to an SVG. The good news is that you don't need to (since we just set the tooltip div to an absolute position). So, append the div to the body:
var div = d3.select("body")
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0.7);
Third problem: set the pointer-events to none or move the tooltip a little bit to the right, otherwise it will get in the way of your mouseover event:
.style('left', d3.event.pageX + 10 + 'px')
This is your updated CodePen: http://codepen.io/anon/pen/GrqKBY?editors=0110
http://codepen.io/anon/pen/YNWKpr
var div = svg.append('foreignObject').append('xhtml:div')
.attr('class', 'tooltip')
.style('opacity', 0.7);
You have to wrap non-svg elements in a foreignObject tag, and you have to specify the html namespace when appending html elements.
I have a D3 multi-line graph that uses the legend for onclick mouseover and mouseout events. Clicking on the legend will hide the line. Mousing over the legend will make the line bold and mousing out will put the line back to normal.
The problem is that if I click the legend and then remove the mouse before the D3 transition completes the transition will not finish. If I keep the mouse over the legend long enough for the transition everything works fine.
To test click a legend rectangle and quickly move the mouse out - the line will not disappear.
Fiddle here: https://jsfiddle.net/goodspeedj/5ewLxpre/
The code for the mouse events is below:
.on("click", function(d) {
var selectedPath = svg.select("path." + d.key);
//var totalLength = selectedPath.node().getTotalLength();
if (d.visible === 1) {
d.visible = 0;
} else {
d.visible = 1;
}
rescaleY();
updateLines();
updateCircles();
svg.select("rect." + d.key).transition().duration(500)
.attr("fill", function(d) {
if (d.visible === 1) {
return color(d.key);
} else {
return "white";
}
})
svg.select("path." + d.key).transition().duration(500)
.delay(150)
.style("display", function(d) {
if(d.visible === 1) {
return "inline";
}
else return "none";
})
.attr("d", function(d) {
return line(d.values);
});
svg.selectAll("circle." + d.key).transition().duration(500)
//.delay(function(d, i) { return i * 10; })
.style("display", function(a) {
if(d.visible === 1) {
return "inline";
}
else return "none";
});
})
.on("mouseover", function(d) {
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "4px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 4 })
// Fade out the other lines
var otherlines = $(".line").not("path." + d.key);
d3.selectAll(otherlines).transition().duration(200)
.style("opacity", 0.3)
.style("stroke-width", 1.5)
.style("stroke", "gray");
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 0.3)
.style("stroke", "gray");
})
.on("mouseout", function(d) {
d3.select(this)
.attr("height", 10)
.attr("width", 25)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "1.5px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 2 })
// Make the other lines normal again
var otherlines = $('.line').not("path." + d.key);
d3.selectAll(otherlines).transition().duration(100)
.style("opacity", 1)
.style("stroke-width", 1.5)
.style("stroke", function(d) { return color(d.key); });
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 1)
.style("stroke", function(d) { return color(dimKey(d)); });
});
Thanks in advance.
You could assign a class to your legend when it's clicked (.clicked), then call setTimeout with an appropriate delay to remove that class once the transition is complete.
When you mouseover or mouseout, first check to see if the legend has the .clicked class. If so, set some delay value as suggested in the other answer, otherwise, proceed without a delay. The advantage of this compared to the other answer is that there would only be a delay if it's needed.
EDIT
If your legend has the class ".legend", modify your code as follows:
.on("click", function(d) {
// Add .clicked class to the legend
$('.legend').addClass('clicked');
// remove clicked class after 750ms. Your duration is 500ms,
// so I'm padding it a bit although you can adjust this as needed
setTimeout(function () { $('.legend').removeClass('clicked') }, 750);
... rest of your function
})
.on("mouseover", function(d) {
// check if legend has been clicked recently and change delay if so
var transitionDelay = 0;
if($('.legend').hasClass('clicked')) transitionDelay = 750;
// your function
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().delay(transitionDelay).duration(200)
.style("stroke-width", "1.5px");
... rest of your function
});
When you have multiple transitions, one can interrupt another. What is happening with your code is that the onclick transition get interrupted by the mouseout transition. This results in the lines not showing showing up. To fix this, just add delay to your mouseout event, so that it occurs after the onclick event has completed. For example, I made the following changes:
added a delay to line 295:
d3.select("path." + d.key).transition().delay(300).duration(200)
.style("stroke-width", "1.5px");
and on line 244 reduced your onclick delay to 200 from 500, just for this test,
svg.select("path." + d.key).transition().duration(200)
.delay(150)
I have a set of graphs that can be dynamically added and removed from the page. Each one has an invisible 'rect' element appended to the base svg hosting each graph, and on that rect element I can append mouseover elements. However, these are all limited to the single svg/rect that the mouse is hovering over; I'd like to extend them to cover all visible graphs. Here's the main code affecting that:
var focus = svg.append('g') // An invisible layer over the top. Problem is, it only overlays on one graph at a time...
.style('display', 'none');
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
// append the x line
focus.append("line")
.attr("class", "x")
.style("stroke", "blue")
.style("stroke-dasharray", "3,3")
.style("opacity", 0.5)
.attr("y1", 0)
.attr("y2", height);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(dataset, x0, 1),
d0 = dataset[i - 1],
d1 = dataset[i],
d = x0 - d0.time > d1.time - x0 ? d1 : d0;
focus.select(".x")
.attr("transform", function() {
return "translate(" + x(d.time) + "," + rightDomain(symbol, d) + ")";
})
.attr("y2", height - y(d[symbol]));
}
All of this is inside a forEach() loop, where it loops over an array containing the names of the graphs to be shown, so multiple graphs (albeit in their separate svgs) show up.
I also have a plunker here: http://plnkr.co/edit/s4K84f5HGRjHFWMwiuIA?p=preview. I'm not sure why it's failing to work since I've copied and pasted my code, which I know works elsewhere.
Edit: I've managed to attach another svg element to the body but for some reason I can't get it to overlay on top of the existing svgs (the graphs). Here's my code (where I've tried several ways of adjusting the position):
var overlay = d3.select('html')
.append('div')
.attr('height', function() { return (symbols.length - 1) * 135 + 130; })
.attr('width', 1000)
.attr('z-index', 2)
//.attr('transform', 'translate(' + margin.left + ',' + extraBuffer/2 + ')');
//.attr('x', margin.left)
//.attr('y', extraBuffer/2);
.attr('position', 'absolute')
.attr('top', '20')
.attr('right', '40');
Looking at this in chrome devtools I always see it below existing graphs, even if I explicitly set its x/y values.
I've been looking for a way to have my legend control my chart animation (similar to NVD3 examples). I've run into a problem though - nested selections.
var legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(70,10)")
;
var legendRect = legend.selectAll('rect').data(colors);
legendRect.enter()
.append("rect")
.attr("x", w - 65)
.attr("width", 10)
.attr("height", 10)
.attr("y", function(d, i) {
return i * 20;
})
.style("stroke", function(d) {
return d[1];
})
.style("fill", function(d) {
return d[1];
});
I'm using a bit of a hack to do my animation. Basically setting style to display: none.
I want to be able to click on the rectangles and call the function. But putting a mouseover or onclick within legendRect doesn't work. The bars to animate are not children of the legend. How can I call the function, or chain my function to my legend?
function updateBars(opts) {
var gbars = svg.selectAll("rect.global");
var lbars = svg.selectAll("rect.local");
if (opts === "global") {
gbars.style("display", "block") ;
lbars.style("display", "none");
gbars.transition()
.duration(500)
.attr("width", xScale.rangeBand());
};
if (opts === "local") {
lbars.style("display", "block")
;
gbars.style("display", "none");
lbars.transition()
.duration(500)
.attr("x", 1 / -xScale.rangeBand())
.attr("width", xScale.rangeBand());
};
}
My other obstacle is changing the fill color on click. I want it to almost imitate a checkbox, so clicking (to deselect) would turn the fill white. I tried something similar as .on("click",(".style" ("fill", "white"))). But that is incorrect.
Here is my fiddle. For some reason, the function isn't updating things on Fiddle. It works on my localhost though. Not sure the problem with that.
I'm not completely sure I understand you correctly, but if your first question is how to change element X when clicking on element Y, you need something along the lines of:
legendRect.on("click", function() {
gbars.transition()
.duration(500)
.style("display", "block")
// etc...
}
As for changing the fill on click, try:
gbars.on("click", function() {
d3.select(this)
.attr("fill", "white");
}
My quest for d3.js wisdom continues!
This time, I have added a guide line which is hovering in a vertical direction as a tool close to the pointer. The problem is that the line disturbs the mousemove functions since it adds an extra layer on top of the rest of the graph, which makes the the code run the mouseout event on sudden pointer movements. Is there a solution for this?
I have implemented the function in the following manner:
svg.on("mousemove", function(d) {
svg.select(".guideline").remove();
//svg.select(".valuelabel").remove();
svg.append("line")
.attr("class", "guideline")
.attr("x1", d3.mouse(this)[0]-3)
.attr("x2", d3.mouse(this)[0]-3)
.attr("y1", margin[0])
.attr("y2", height+margin[0])
.attr("opacity", originOpacity)
.attr("stroke", "#333");
});
And as an example of an event it is disturbing:
//Highlight each stack when hovering, and calculate y value for legend
stacks.on("mousemove", function(d) {
svg.select(".label").remove();
//Calculate the closest index when hovering
var perValue = width / data[0].data.length;
var index = Math.ceil((d3.mouse(this)[0]-margin[3]) / perValue - 0.5);
chart.selectAll(".valuelabel").each(function(data) {
if (data.name == d.name) {
d3.select(this).text(Math.round(data.data[index].y) + "%");
}
});
d3.select(this).attr("opacity", "1");
svg.selectAll("." + d3.select(this).attr("class")).attr("opacity", "1");
svg.append("text")
.attr("class", "label")
.attr("width", "100px")
.attr("height", "20px")
.attr("x", d3.mouse(this)[0] + 40)
.attr("y", d3.mouse(this)[1] - 5)
.text(d.group + ": " + d.name);
});
stacks.on("mouseout", function(d) {
groups.selectAll("." + d.name).text(d.name);
svg.select(".label").remove();
svg.selectAll("." + d3.select(this).attr("class")).attr("opacity", originOpacity);
});
Looks like you want pointer-events none on the guide line.