I have a d3 line that is only one pixel wide. I want to have a on-click
handler assigned to this line. However it is very hard to be exactly on top of the line when trying to select it. Is there some "selection corridor" feature that lets you specify a "distance to line" instead. I.e. the click event is fired if the mouse pointer is, say, in a distance of max 8pt away from a line and the mouse is clicked?
The solution in the other answer is a good solution and an interesting one.
However, I prefer the "traditional" solution, which is painting another line, transparent and thicker than the visible thin line, just to catch the click event. And the reason I prefer this "traditional" solution (the most frequent one among D3 coders) is UX: it's a good idea letting the user know that she/he can click the line.
Thus, the advantage of having a thicker, transparent line over the thin, visible line is that you can set the cursor to a hand...
.attr("cursor", "pointer")
... when the user hover over the transparent line.
Here is a simple demo:
var svg = d3.select("svg");
var data = d3.range(30).map(function(d) {
return {
x: d * 10,
y: Math.random() * 150
}
});
var lineGenerator = d3.line()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
});
var realLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("d", lineGenerator(data));
var transparentLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 12)
.attr("opacity", 0)
.attr("cursor", "pointer")
.attr("d", lineGenerator(data));
transparentLine.on("click", function() {
console.log("clicked")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Of course, this "traditional" solution only makes sense if you have one or few lines on that chart. If you have dozens or hundreds of lines, it will be a complete mess.
You can attack click event on svg/group element and check if the distance from the mouse position and the closest point on the path is less than some fixed value, e.g. 4. The distance can be calculated using the function from Closest Point on Path example.
Click event:
var path = svg.append('path').datum(data).attr("d", line);
svg.on('click', function() {
var m = d3.mouse(this),
p = closestPoint(path.node(), m);
if (p.distance < 4) {
/* make action */
console.log('click', p.distance)
}
})
closestPoint() is taken from the example above.
example: https://jsfiddle.net/xny9bx4v/
Related
I've created a DOM element which, when it fires a mouseover event, creates new DOM elements such as labels or a tooltip. Unfortunately, sometimes those elements are created underneath the mouse's current position. This causes the mouseleave event of that DOM element to fire, which is often responsible for removing the newly created DOM element.
In other words, when the new elements are created, the mouse is no longer "hovering" over the DOM element that originally fired the event, but the new DOM element. The browser reads this as a "mouseleave" event, which then fires the "mouseleave" function. This event, as it so happens, then removes the new element – even though the user has not moved the mouse.
This causes a cycle to occur, where the new elements are created and removed in rapid-fire, causing the blinking effect when a certain part of the DOM element is moused over. Here's a simplified version of the problem:
https://jsfiddle.net/KingOfCramers/8wbfjap4/2/
var svg = d3.select("svg").style("background-color","grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",100)
.attr("cy",100)
.attr("r", 20)
.style("fill","red")
circle
.on("mouseover",function(d){
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x",xPos)
.attr("y",yPos)
})
.on("mouseleave",function(){
d3.select("text").remove();
})
Obviously this example is silly, but in instances where the data is much more crowded, simply moving the label by 10 or 15 pixels up or down is not a practical solution. I also cannot just create the label relative to the mouse cursor, because I will often be creating many at once using D3 data, for multiple DOM elements. What do most people do to solve this issue?
Thanks.
If you don't need to interact with that new element, just use pointer-events: none;:
.attr("pointer-events", "none")
Here is your code with that change:
var svg = d3.select("svg").style("background-color", "grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
circle
.on("mouseover", function(d) {
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x", xPos)
.attr("y", yPos)
.attr("pointer-events", "none")
})
.on("mouseleave", function() {
d3.select("text").remove();
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width=500 height=500></svg>
I'm trying to include tooltips on a map I'm making in D3, imitating this code:
http://bl.ocks.org/lhoworko/7753a11efc189a936371
And here is the map I'm working on:
https://pantherfile.uwm.edu/schro333/public/2016_electoral_map/
As you can see here, I have tooltips working, and they display the correct name when the user hovers over a state, but the position relative to the cursor is really off. I'm not sure why this is.
Relevant code:
svgContainer.selectAll("pathCodes")
.data(json.features)
.enter()
.append("path")
.attr("id",
function(d){
var stateNameId = d.properties.name.toString();
stateNameId = stateNameId.replace(/\s+/g, '');
return stateNameId;
}) // this function returns the name of the state with spaces stripped and assigns it to individual polygon as id
.attr("d", pathCodes)
.attr("stroke", "black") // state outline color
.attr("stroke-width", "1") // state outline width
.attr("class", "noparty") // default to no party
.style("fill", politicalParties[0].color) // default fill is that of no party
/////////////
.on('mousemove', function(d) {
var mouse = d3.mouse(svgContainer.node());
tooltip.classed('hidden', false)
.attr('style', 'left:' + (mouse[0]) +
'px; top:' + (mouse[1]) + 'px')
.html(d.properties.name);
})
.on('mouseout', function() {
tooltip.classed('hidden', true);
});
/////////////
You get the wrong position because the X/Y position you are using is based off the SVG and not the actual location of the SVG on the page.
You can use
var loc = document.getElementById("states-map").getBoundingClientRect();
console.log(loc.top); //add this to the top
to get the offset. Not sure the d3 way to do it.
I use a function to set up event handlers for a click and as it gets fired, the rendition goes as supposed to (fold in on the outer control and fold out on the inner one). Then, clicking it again, the process is retracted. However, the next time I perform the operation, only the outer component changes its size while the inner one does not get affected.
function pieClickOuter(target) {
var pie = d3.layout.pie()
.startAngle(0).endAngle(2 * Math.PI)
.value(function (d) { return d.val; });
var out = d3.svg.arc().innerRadius(90).outerRadius(99);
var org = d3.svg.arc().innerRadius(1).outerRadius(1);
var sub = d3.svg.arc().innerRadius(10).outerRadius(80));
d3.selectAll("#chart .sector path")
.transition().duration(1500).attr("d", out);
var grx = _.chart.selectAll(".subSector")
.data(pie(getData())).enter().append("g")
.attr("class", "subSector")
.on("click", pieClickInner);
grx.append("path")
.attr("d", org).style("fill", function (d) { return colors(d.value); });
grx.selectAll("#chart .subSector path")
.transition().duration(1000).attr("d", sub);
}
function pieClickInner() {
d3.selectAll("#chart .sector path")
.transition().duration(1500)
.attr("d", d3.svg.arc().innerRadius(80).outerRadius(99));
outerPieEvents(d3.selectAll("#chart .sector"));
d3.selectAll("#chart .subSector path")
.transition().duration(1000)
.attr("d", d3.svg.arc().innerRadius(1).outerRadius(1));
}
I cannot for my life see why. According to the console output, all the steps are executed, so it seems that the events are re-set up correctly. Still, the inner component seems to disobey.
See this fiddle
I believe that you want something like this.
I changed only 1 name:
var grx = _.chart.selectAll(".subSector")
to
var grx = _.chart.selectAll(".foo")//or any other name
so, we don't select what already exists.
The problem with this approach is that your SVG will have more and more groups each click. But you can avoid this removing them in your pieClickInner():
d3.selectAll("#chart .subSector path")
.transition().duration(1000)
.attr("d", d3.svg.arc().innerRadius(1).outerRadius(1)).remove();
d3.selectAll("#chart .subSector text").attr("opacity", 0).remove();
d3.selectAll("g.subSector").transition().duration(1000).remove();
I, personally, don't like remove(), i'd simply rebind the data.
I'm trying to surround the mouse point with a circle, much like a crosshair, and have this circle track the mouses movement. So far the best strategy I have is using D3 enter-update-exit:
Append circle on mouse point underpinned by data.
on mouse move add another circle to data array with data = new mouse point.
if data array exceeds 1, shift() the first value out.
update visualisation.
jsfiddle here - http://jsfiddle.net/hiwilson1/kur2bbv9/1/ - though I think it's largely irrelevent as this strategy is fundamentally flawed. The circle appears as though it's lagging behind the cursor and flickers. A lot. Which I don't want.
Key part of code here:
function onMove() {
var m = d3.mouse(this);
var point = {x: m[0], y: m[1]};
area.push(point)
document.getElementById("svg").onmousedown = function() {
mouseDown++;
addNode(m);
};
document.getElementById("svg").onmouseup = function() {
mouseDown--;
};
if (mouseDown > 0) {
addNode(m);
}
//if theres two circles, remove the first leaving just the second.
if (area.length > 1) {
area.shift();
}
var crosshair = svg.selectAll(".area")
.data([area])
crosshair
.attr("class", "area")
.attr("cx", m[0])
.attr("cy", m[1])
.attr("fill", "none")
.attr("stroke", "grey")
.attr("stroke-width", "3px")
.attr("r", 30)
crosshair.enter()
.append("circle")
.attr("class", "area")
.attr("cx", m[0])
.attr("cy", m[1])
.attr("fill", "none")
.attr("stroke", "grey")
.attr("stroke-width", "3px")
.attr("r", 30)
crosshair.exit().remove()
};
Is there another way of accomplishing this? Happy to accept non D3 strategies.
I couldn't get your JSfiddle to display anything, so I'm not sure if I'm totally missing the point, but could you just use a custom CSS cursor on top of your SVG element? It seems that .cur cursor files have the most wide-spread support. That would be a native alternative for custom hacks (thus giving better performance), and it would also degradate gracefully on un-supported browsers.
In Mike Bostocks example http://bost.ocks.org/mike/nations/ there is so much data that putting the names of the countries there would make it chaotic, but for a smaller project I would like to display it.
I found this in the source:
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter().append("circle")
.attr("class", "dot")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(position)
.sort(order);
dot.append("title")
.text(function(d) { return d.name; });
But somehow a title never shows up. Does anybody have an idea, how to display the name, next to the bubble?
As the other answer suggests, you need to group your elements together. In addition, you need to append a text element -- the title element only displays as a tooltip in SVG. The code you're looking for would look something like this.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter()
.append("g")
.attr("class", "dot")
.call(position)
.sort(order);
dot.append("circle")
.style("fill", function(d) { return color(d); });
dot.append("text")
.attr("y", 10)
.text(function(d) { return d.name; });
In the call to position, you would need to set the transform attribute. You may have to adjust the coordinates of the text element.
Unfortunately grouping the text and circles together will not help in this case. The bubbles are moved by changing their position attributes (cx and cy), but elements do not have x and y positions to move. They can only be moved with a transform-translate. See: https://www.dashingd3js.com/svg-group-element-and-d3js
Your options here are:
1) rewrite the position function to calculate the position difference (change in x and change in y) between the elements current position and its new position and apply that to the . THIS WOULD BE VERY DIFFICULT.
or 2) Write a parallel set of instructions to setup and move the tags. Something like:
var tag = svg.append("g")
.attr("class", "tag")
.selectAll(".tag")
.data(interpolateData(2004))
.enter().append("text")
.attr("class", "tag")
.attr("text-anchor", "left")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(tagposition)
.sort(order);
You will need a separate tagposition function since text needs 'x' and 'y' instead of 'cx', 'cy', and 'r' attributes. Don't forget to update the "displayYear" function to change the tag positions as well. You will probably want to offset the text from the bubbles, but making sure the text does not overlap is a much more complicated problem: http://bl.ocks.org/thudfactor/6688739
PS- I called them tags since 'label' already means something in that example.
you have to wrap the circle element and text together , it should look like
<country>
<circle ></circle>
<text></text>
</country>