I am trying to write a function that adds a class to all elements in a selection when those elements do not have that class yet, and vice versa:
function toggleLinksActivity(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", l => !l.classed("non-active"));
}
This gives an error that .classed
is not a function. The documentation states that .classed
should be called on a selection, so I tried changing the last line into !d3.select(l).classed("non-active")
, but that does not work either ("t.getAttribute is not a function"). What is going wrong and what is the correct way?
Note: I have already solved my problem with the 2 separate functions underneath, but I find it ugly to split a toggle into 2 parts.
function activateLinks(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", false);
}
function deactivateLinks(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", true);
}
I am trying to write a function that adds a class to all elements in a selection when those elements do not have that class yet, and vice versa:
function toggleLinksActivity(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", l => !l.classed("non-active"));
}
This gives an error that .classed
is not a function. The documentation states that .classed
should be called on a selection, so I tried changing the last line into !d3.select(l).classed("non-active")
, but that does not work either ("t.getAttribute is not a function"). What is going wrong and what is the correct way?
Note: I have already solved my problem with the 2 separate functions underneath, but I find it ugly to split a toggle into 2 parts.
function activateLinks(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", false);
}
function deactivateLinks(d) {
d3.selectAll(".link")
.filter(l => l.target == d)
.classed("non-active", true);
}
Share
Improve this question
edited May 5, 2018 at 23:43
Gerardo Furtado
102k9 gold badges128 silver badges177 bronze badges
asked May 5, 2018 at 15:50
JeroenJeroen
5443 silver badges28 bronze badges
3
-
It looks like your logic is ok -- but i suspect the filter parison
l.target == d
is not returning anything. Hard to tell without knowing what your inputd
contains, and whatdata(...)
has been attached to your links... – SteveR Commented May 5, 2018 at 16:42 - @SteveR I checked in the console that the filtering is done correctly. – Jeroen Commented May 5, 2018 at 16:48
-
3
l
is your datum, you need to select your element to be able to class the selection properly, this is whyl.classed()
andd3.select(l).classed
do not work, you aren't using .classed() on a d3 selection in either approach. – Andrew Reid Commented May 5, 2018 at 16:59
3 Answers
Reset to default 5Gerardo's answer is pointing in the right direction by using Element.classList
, but the proposed solution can still be simplified. As it turns out, the classList
property already features a .toggle()
method, which works as follows:
toggle( String [, force] )
When only one argument is present: Toggle class value; i.e., if class exists then remove it and return false, if not, then add it and return true. When a second argument is present: If the second argument evaluates to true, add specified class value, and if it evaluates to false, remove it.
Your function can thus be written as:
function toggleLinksActivity(d) {
d3.selectAll(".link")
.each(function(l) {
if (l.target == d) {
this.classList.toggle("non-active");
}
});
}
You need to reselect the ith element and use its classed
property:
function toggleLinkActive() {
d3.selectAll(".link")
.classed("non-active", (d,i,nodes) => !d3.select(nodes[i]).classed("non-active"));
}
You can also select this
if you are using a non-arrow function:
function toggleLinkActive() {
d3.selectAll(".link")
.classed("non-active", function() { return !d3.select(this).classed("non-active"); });
}
If you want to avoid reselecting the item, you can use each
and store the selection in a variable:
function toggleLinkActive() {
d3.selectAll(".link")
.each(function() {
const item = d3.select(this);
item.classed("non-active", !item.classed("non-active"));
});
Or you can use the native this.classList.toggle("non-active")
like in @altocumulus' answer.
Instead of using the D3 classed
method again inside itself we can use classList, toggling the class with a more short and elegant function that uses the unary negation and contains
to check if the selection has a given class.
In my proposed function, you have to pass the selection and the name of the class. It's just this:
function toggleClass(selection, className) {
selection.classed(className, !selection.node().classList.contains(className))
}
Explanation
The logic behind this function is quite simple: contains()
returns a boolean... so, negating it (!true
gives us false
and !false
gives us true
) and passing that negated boolean to the classed
method toggles the class in the selection.
Let's see it in action:
var paragraphs = d3.selectAll("p");
d3.interval(function() {
toggleClass(paragraphs, "foo");
console.log(paragraphs.attr("class"))
}, 1000)
function toggleClass(selection, className) {
selection.classed(className, !selection.node().classList.contains(className))
}
<script src="https://d3js/d3.v5.min.js"></script>
<p class="foo someOtherClass">Foo</p>
<p class="foo someOtherClass">Bar</p>
<p class="foo someOtherClass">Baz</p>