Change style after a d3 path has been clicked - javascript

How can I stop the path style being changed if the path has been clicked? I want path style to not be changed after pathHover has been clicked.
let pathHover = this.svg.append('path')
.data([data])
.attr('class', 'line-padded')
.attr('d', line);
pathHover.on('mouseover', function() {
console.log('path mouse over');
path.style('stroke-width', 6);
});
pathHover.on('mouseleave', function() {
path.style('stroke-width', 4);
});
pathHover.on('click', function() {
console.log('clicked');
path.style('stroke', 'blue');
path.style('stroke-width', 6);
});

There are different ways to achieve this. Since the first D in DDD (also known as D3) means data, the approach I like most is binding a datum to the clicked element, indicating that it was clicked:
d.clicked = true;
Or, if you want to reverse the boolean after a second click:
d.clicked = !d.clicked;
Then, in the mouseover, just check that datum:
if (d.clicked) return;
Here is a demo using green circles: if you mouse over them, they turn red. If you click them, they turn blue, and never turn red (or green) again.
var svg = d3.select("svg");
var circles = svg.selectAll(null)
.data(d3.range(5).map(function(d) {
return {
x: d
}
}))
.enter()
.append("circle")
.attr("cursor", "pointer")
.attr("cy", 75)
.attr("cx", d => 30 + 50 * d.x)
.attr("r", 20)
.style("fill", "lime");
circles.on("mouseover", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "firebrick")
}).on("mouseout", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "lime")
}).on("click", function(d) {
d.clicked = !d.clicked;
d3.select(this).style("fill", "blue")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Probably a few tactics, either
set path.style("pointer-events", "none") for clicked paths, which will stop all future clicks/mouseover events.
Or if that is too drastic, add a class to clicked paths path.classed("clicked", true), which you could use in a test during your mouseover event before applying any styling changes

Related

Remove Non-SVG Slider

I have code for a slider:
var slider = d3.select('body').append('p').text('Sent or Received Threshold: ');
slider.append('label')
.attr('for', 'threshold')
.text('');
slider.append('input')
.attr('type', 'range')
.attr('min', d3.min(graph.links, function(d) {return d.value; }))
.attr('max', d3.max(graph.links, function(d) {return d.value; }))
.attr('value', d3.min(graph.links, function(d) {return d.value; }))
.attr('id', 'threshold')
.style('width', '100%')
.style('display', 'block')
.on('input', function () {
var threshold = this.value;
d3.select('label').text(threshold);
var newData = [];
graph.links.forEach( function (d) {
if (d.value >= threshold) {newData.push(d); };
});
color.domain([d3.min(newData, function(d) {return d.value; }), d3.max(newData, function(d) {return d.value; })]).interpolator(d3.interpolateBlues);
link = link.data(newData, function(d){ return d.value});
link.exit().remove();
var linkEnter = link.enter().append("path")
.style("stroke", function(d) { return color(d.value); })
.style("fill", "none")
.style("stroke-width", "3px");
link = linkEnter.merge(link).style("stroke", function(d) { return color(d.value); });
node = node.data(graph.nodes);
simulation.nodes(graph.nodes)
.on('tick', tick)
simulation.force("link")
.links(newData);
simulation.alphaTarget(0.1).restart();
});
I would like to remove the slider within a change function based on a PHP drop-down. For the d3 viz itself, I use d3.selectAll("svg > *").remove(). I know this slider isn't part of SVG, so how would I remove it? Currently, on change of the drop-down, a new slider is added below the previous slider. Is there a d3.selectAll statement that needs to be placed somewhere?
Thank you for any insight you all might have!
D3 is not restricted to manipulating SVG elements. In fact, D3 can manipulate anything in that page.
That being said, you can do that by simply using:
selection.remove()
Where selection is, of course, any selection containing the slider.
For instance, you can select by element...
d3.select("input").remove();
... or by ID:
d3.select("#threshold").remove();
But, in your case, the simplest solution is using the selection you already have:
slider.remove();
Here is a demo with your code for the slider, click the button:
var slider = d3.select('body').append('p').text('Sent or Received Threshold: ');
slider.append('label')
.attr('for', 'threshold')
.text('');
slider.append('input')
.attr('type', 'range')
.attr('min', 0)
.attr('max', 100)
.attr('value', 30)
.attr('id', 'threshold')
.style('width', '100%')
.style('display', 'block');
d3.select("button").on("click", function(){
slider.remove();
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Click me</button>

D3 onclick, onmouseover and mouseout behavior overriding

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)

Change the color of nodes on double click in D3

I want the color of node to change on double click. i.e. On the first double click, it would turn black but on re-double clicking it, it would turn back to its original color. I am able to make it turn black on the first double click, but I'm not able to make it turn back to its original color. Thanks in advance and here is my code.
var gnodes = svg.selectAll('g.gnode')
.data(graph.nodes)
.enter()
.append('g')
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) {return d.colr; })
.on('dblclick', function (d)
{
if (d3.select(this).style("fill") != "black")
{
d3.select(this).style("fill", "black");
}
else
{
d3.select(this).style("fill", function(d){return d.colr;});
}
})
.call(force.drag);
The issue you're having here is actually really tricky to spot.
You are checking whether the fill style is equal to the string "black". The problem is, many browsers (including Chrome and FF) reformat color names to RGB strings. So, when you set the fill to "black", it is converted to the RGB string "rgb(0, 0, 0)". So actually, calling d3.select(this).style("fill") will return this rgb string rather than the color name, ensuring that the else branch of your code never runs.
You can avoid having to check the current state of your fill as a style string by using selection.each to store each circle's state as a boolean value and then register its double-click handler, which toggles the boolean and then branches based on its value. Try this:
var node = gnodes.append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) {return d.colr; })
.each(function() {
var sel = d3.select(this);
var state = false;
sel.on('dblclick', function() {
state = !state;
if (state) {
sel.style('fill', 'black');
} else {
sel.style('fill', function(d) { return d.colr; });
}
});
});
One way to handle this is via CSS:
.doubled { fill: black !important; }
Then toggle this CSS class in your dblclick function:
d3.selectAll(".node")
.on("dblclick", function() {
var c = d3.select(this).classed("doubled");
d3.select(this).classed("doubled", !c);
})
Working example here: http://jsfiddle.net/qAHC2/829/

modify circle elements by event listener on text matching id of circle

first of all sorry for the long title, I couldn't come up with a better one to explain my problem.
What I have is a scatterplot, with circles that have different ids based on the d.FamilyName column in my csv. Some circles share the same id.
On the right of that scatterplot, I set up a div, containing a list of all the d.Familyname values, nested to have them show just once. The id of every text is also set by d.FamilyName.
To improve readability of the scatterplot, since it has many values, I was planning to add an event listener on mouseover on the text, which should then modify the radius of the circles sharing the same id as the text.
Every circle is plotted inside the var circle and the text is plotted inside a div, here's the code for the circle and the text part:
var circle = svg.append("g")
.attr("id", "circles")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) { return x(d.SquadraturaInterna_o); })
.attr("cy", function (d) { return y(d.SquadraturaEsterna_o); })
.attr("r", 2)
After the radius attribute there's an event listener to show other values of the specified circle (the name and coordinates) but they are not relevant to my problem I think.
The text part:
d3.select("#elenco")
.select("#value")
.selectAll("text")
.data(nested)
.enter()
.append("p")
.append("text")
.attr("id", function (i) { return (i).key; })
.text(function (i) { return (i).key; })
.on("mouseover", function (d, i) {
if (this.id == circle.id)
{d3.select("circle")
.attr("r", 5);
}
else {d3.select("circle").attr("r", 1);}
;})
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2);
});
The problem is of course on the if statement in the mouseover.
Any hint on how to solve this would be really appreciated, thanks!
EDIT: Thanks to #Lars' reply I was able to make this work with a little edit to his code, like so:
.on("mouseover", function (d) {
var sel = this.id;
circle.filter(function() { return this.id === sel; }).attr("r", 5); })
.on("mouseout", function (d, i) {
d3.selectAll("circle")
.attr("r", 2); }
);
As an alternative to #musically_ut's approach, you can also use the .filter() function to modify only the elements you want.
.on("mouseover", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 5);
})
.on("mouseout", function(d) {
circle.filter(function() { return this.id === d.FamilyName; }).attr("r", 1);
})
I think you are looking for this:
.on("mouseover", function (d, i) {
circles.attr('r', function (circle_d, i) {
return (d.id == circle_d.id) ? 5 : 1;
});
})
In D3, the this in the accessor functions refers to the DOM element. If you wanted to compare the id of the DOM element with circle's data's ids, then you could do something like this kind: d3.select(this).attr('id') == circle_d.id (cache d3.select(this).attr('id') for performance reasons).

Interactive Legend onclick or mouseover - D3js

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");
}

Categories

Resources