I'm trying to get d3 to change the color of plot points upon clicking them, but can't seem to get this working at the moment. The commented line below does change the color from white to magenta, but the toggleColor function does not seem to do anything. Actually, the alert only happens when first run, and not when a point is clicked. What am I doing wrong here?
var circle = graph.selectAll("circle.value")
.data(data)
.enter().append("circle")
.attr("class", "value")
.attr("cx", function(d) { return x(d.hour); })
.attr("cy", function(d) { return y(d.value); })
.attr("r", 5)
//.on("click", function(){d3.select(this).attr("class", "flagged");});
.on("click", toggleColor);
var toggleColor = (function(){
// throw in an alert for good measure. . .
alert("Clicked?")
var currentColor = "white";
return function(){
currentColor = currentColor == "white" ? "magenta" : "white";
d3.select(this).atrr("class", "flagged");
}
})();
To begin with, var toggleColor is still undefined at the point where you're wiring up the click event (because it's defined further down the page). So you need to move it up.
Then, the reason the alert appears only once, at runtime, is because that's when that code is run. If you notice, the outer function is executed right after it's declared, as evident by the () at the very last line of the code. That's when alert() gets called. You'll want to move it into the body of the inner function -- the one that's returned -- because that inner function is the code that will actually run on click.
Related
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/
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 am working with a scatterplot in d3. Dots on the graph represent a paper. On right click of a dot I have a context menu where there are 2 options: 1) to add that paper to the library (change type to In_library) and 2) Remove from library (remove paper from data completely).
I call the refreshGraph() function after each of these updates which redraws the graph with the updated data. But nothing happens which I assume is because the refreshGraph() is not being called properly? Or for option 1 type library is not being set properly? When refreshGraph is called after option 1 the dot should turn blue and on calling it for option 2 the dot should disappear from display as it has been removed from the alldata which is the data that is being used to draw the circles. Here is the relevant code:
allData = [];
var menu = [{
title: 'Add to Library',
action: function addToLibrary(elem, d, i) {
d3.json("connection6.php?paperID="+d.ID, function(error, dataJson) {
for(i=0;i<allData.length;i++){
if (d.type === "In_library")
{
alert("The paper: " + d.TITLE + " is already in your Library!");
return;
}
}
d.type = "In_library"; // is this the correct way to change the type if the input has a different type??
refreshGraph();
})
refreshGraph();
}
},
{
title: 'Remove from Library',
action: function removeFromLibrary (elem, d, i) {
d3.json("connection9.php?paperID="+d.ID, function(error, dataJson) {
//loop through allData and if selected ID has type In_library, remove from allData
for(i=0;i<allData.length;i++){
if (d.type == "In_library"){
allData.splice(i--,1);
}
}
refreshGraph();
})
}
}
]
function refreshGraph() {
// draw dots
var circles = svg.selectAll("circle")
.data(allData)
circles.transition()
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
circles.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill",function(d){
var colour = "black"
switch(d.type){
case "In_library":
colour = "blue";
break;
case "cited by":
colour = "red";
break;
case "cites":
colour = "green";
break;
case "selected":
colour = "magenta";
break;
default:
colour = "black";
}
return colour;
})
.on("mouseover", mouseHandler)
.on("mouseout", mouseoutHandler)
.on("click", clickHandler)
.on("contextmenu", rightClickHandler);
svg.select(".y.axis").call(yAxis);
svg.select(".x.axis").call(xAxis);
//don't want dots overlapping axis, so add in buffer to data domain
x.domain([d3.min(allData, YearFn)-1, d3.max(allData, YearFn)+1]);
y.domain([d3.min(allData, Num_citationsFn)-1, d3.max(allData, Num_citationsFn)+1]);
}
Any help is much appreciated I am new to d3 so thanks in advance!
You don't need to re-plot all the data each time a single point changes. Just update that one point.
function rightClickHandler() {
// if option 1
d3.select(this).style("fill", "blue");
// if option 2
d3.select(this).remove();
}
Your problem likely arises because when you call refreshGraph a second time (or third) your aren't clearly the circles that are already plotted. Your refreshGraph function isn't updating the points already plotted it's recreating them each time, and if you aren't clearing the points that are already there, you won't see the new points (or the absence of them, or the change in color), because they are hidden behind your old points.
EDIT:
If you want to re-add the data each time, you first have to clear the existing data. At the start of your refreshGraph function, add this line:
if(!d3.selectAll("circle").empty()) d3.selectAll("circle").remove();
i.e. if there are circle elements, remove them. This assumes you are only creating circle elements within the refreshGraph function. If you create them elsewhere, the you should probably use the .dot selector instead.
Hey Guys i need some help to find a way to integrate the switch of images into the fade function. For some reason the chords dont fade after the mouse hovers over the graph.
And to Look at the idea of loading (but not displaying) a series of images during initial page load, and then using the fade function simply to switch a pre-defined image area to show a different image.
This is my JS Bin
You should pass data bonded to the arc and it's index as parameter to the function which is returned by the fade function as shown below.
d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius))
.on('mouseover', function(d,i) {
fade(0.1)(d,i); //Changed line of code
overlayPic.classList.remove('hidden');
overlayPic.src = 'https://farm1.staticflickr.com/697/23125850325_b69a8530dd_n.jpg';
})
.on("mouseout", function(d,i){
fade(1)(d,i); //Changed line of code
overlayPic.classList.add('hidden');
});
Fade function expects that two params.
function fade(opacity) {
return function(g, i) { //Note that this code uses index i
svg.selectAll(".chord path")
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style("opacity", opacity);
};
}
I have a set of randomly plotted diamonds, squares & circles on my canvas. I have one of each lined in a straight line which is created by my go variable. I wish to use the onclick function upon this variable to filter or make the shapes disappear depending on which parameter I give it. e.g. squares will only show squares on the canvas etc.
So far I have started with this basic example:
.on("click", function(d){ if (d.shape == 'square') { return alert('success') ;} })
I then moved onto this:
.on("click", function(d){ if (d.shape =='circle') { return d3.selectAll(".node").filter(function(d) {return d.country === 'USA'} ) } ;})
When I have applied that, it doesnt result to any errors or actions. I'm pretty sure I'm going in the right direction, just would like some help getting there
http://jsfiddle.net/Zc4z9/19/
Thanks, in advance!
You are doing nothing with your selection. If you need to hide it just add .style("display", "none")
.on("click", function(d){
if (d.shape =='circle') {
d3.selectAll(".node")
.filter(function(d) {return d.country === 'USA'} )
.style("display", "none");
}
})