I'm binding data to a bunch of nodes using d3, and I would like to arrange it so that all of the nodes change dynamically when one of them is clicked on (or some other event). Based on my understanding of d3, I think it should work like this:
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
.attr("class", ".node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", function(e, j) {
if(someCondition(i, j))
return "red";
else
return "green";
});
});
But nothing happens when I click. Even the simpler code:
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
.attr("class", ".node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", "red");
});
(which I expect would turn all of the nodes red when one of them is clicked on) does not work.
There is an error in the way you are setting the class names for your circles by calling
.attr("class", ".node")
Doing it this way would set the attribute to class=".node" which is certainly not what you want. Moreover, this would not be a valid class name. See this answer for an explanation of what characters are allowed to form a class name. To select this class name you would have to do a svg.selectAll("..node") having two dots in your selector string.
Having said that, change you code to leave out the dot to make it work:
.attr("class", "node")
Lessons learned:
.attr() takes the attribute's value literally.
When applying a CSS selector, you prefix it with a dot to select a class name.
You need specify the "cx" and "cy" properties of a circle, otherwise you wont see anything.
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
//add cx and cy here:
.attr("cx", function(d) {return d+10;/*just an example*/})
.attr("cy", function(d) {return 2*d+10;/*just an example*/})
.attr("class", "node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", "red");
});
Related
I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level inside values. For the most part, my code works, but for some reason, the j index in the anonymous function for the fill returns an array of repeated circle instead of returning the parent of the current element. I believe this may have something to do with the way I created the svg and selected the elements, but I can't figure it out. Below is an excerpt of my code that shows how I created the svg, the line path and the circles.
var svgb = d3.select("body")
.append("svg")
.attr("id","svg-b")
.attr("width", width)
.attr("height", height)
var gameb = svgb.selectAll(".gameb")
.data(games)
.enter()
.append("g")
.attr("class", "gameb");
gameb.append("path")
.attr("class", "line")
.attr("d", function(d) {return line_count(d.values); })
.style("stroke", function(d) { return color(d.name); })
.style("fill", "none");
gameb.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3)
.style("fill", function(d,i,j) {console.log(j)
return color(games[j].name);});
j (or more accurately, the third parameter) will always be the nodes in the selection (the array of circles here), not the parent. If you want the parent datum you can use:
.attr("fill", function() {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
return color(datum.name);
})
Note that using ()=> instead of function() will change the this context and the above will not work.
However, rather than coloring each circle independently, you could use a or the parent g to color the circles too:
gameb.append("g")
.style("fill", function(d) { return color(d.name); })
.selectAll("circle")
.data(function(d) {return d.values;})
.enter()
.append("circle")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y_count(d.count);})
.attr("r", 3);
Here we add an intermediate g (though we could use the original parent with a few additional modifications), apply a fill color to it, and then the parent g will color the children circles for us. The datum is passed on to this new g behind the scenes.
I would like to set a filter on paths and append text but nothing happens.
var filteredElements = svgContainer.selectAll("path")
//.data(feat.features)
.append("text")
.filter(function (d) {
if (d.properties.myID > 0) {
return true;
};
})
.attr("x", function (d) {
return path.centroid(d)[0];
})
.attr("y", function (d) {
return path.centroid(d)[1];
})
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo");
filteredElements contains 46 elements which are correct but the text is not being appended.
With that code, it works fine but I need the condition in my filter:
svgContainer.selectAll("path[PE='1442']")
.data(feat.features)
.enter().append("text")
.attr("x", function (d) {
return path.centroid(d)[0];
})
.attr("y", function (d) {
return path.centroid(d)[1];
})
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo");
I'm adding this as a second answer because there isn't enough room in a comment, but it suffices as an answer itself.
You have paths drawn on the svg, and you want to draw text for a subset of those paths.
There are two approaches that could be used for this. One is to use a parent g element to hold both path and text:
// Append a parent:
var g = svg.selectAll(null) // we want to enter an element for each item in the data array
.data(features)
.enter()
.append("g");
// Append the path
g.append("path")
.attr("d",path)
.attr("fill", function(d) { ... // etc.
// Append the text to a subset of the features:
g.filter(function(d) {
return d.properties.myID > 0; // filter based on the datum
})
.append("text")
.text(function(d) { .... // etc.
The bound data is passed to the children allowing you to filter the parent selection before adding the child text.
The other approach is closer to what you have done already, but you don't quite have idiomatic d3. We also don't need to re-bind the data to the paths (d3.selectAll("path").data(), instead we can use:
svgContainer.selectAll(null)
.data(feat.features.filter(function(d) { return d.properties.myID > 0; }))
.enter()
.append("text")
.attr("x", path.centroid(d)[0])
.attr("y", path.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo")
As an aside, your initial approach was problematic in that it:
it appends text to path elements directly, which won't render (as you note)
it is binding data to the paths again, for each element in the selection, you are binding an item of the data array to a selected element - since the selection is a sub-set of your paths, but your data is the full dataset, you are likely assigning different data to each path (without specifying an identifier, the ith item in the full dataset is bound to the ith element in the sub-selection).
I have now a solution, I think. My text nodes were inside my path nodes. Now I'm just doing this in my if condition and add my text node under my paths.
svgContainer.selectAll("path")
.data(feat.features)
.filter(function (d) {
if (d.properties.myID > 0) {
d3.select("svg")
.append("text")
.attr("x", path.centroid(d)[0])
.attr("y", path.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo")
};
})
Here is something similar to what is happening in my code : https://codepen.io/anon/pen/zJmvXa?editors=1010
Press the Update button to see the issue.
The problem in this codePen, is its only redrawing the first element (Myriel).
I think it must be around the enter,exit or the merge, but I don't fully grasp what's going on.
I thought the merge was to merge existing data with new, and the only data that should go into the enter should be the new data. And the exit is for removing redundant data?
As this graph has both circles and text, should the merge be made on the 'g' element that contains these ?
Perhaps it's to do with the data function :
.data(dataset1.nodes, function(d, i) {
return d;
});
If i change this to use d.id, it re renders everything again. I presume it's due to the data having an id attribute. How is this suppose to be done ?
// Apply the general update pattern to the nodes.
forceNetwork.node = d3
.select("#nodesContainer")
.selectAll("g.network-node")
.data(dataset1.nodes, function(d, i) {
return d
});
forceNetwork.node.exit().remove();
forceNetwork.nodeEnter = forceNetwork.node
.enter()
.append("g")
.attr("id", function(d, i) {
return "network-g-node-" + d.id;
})
.attr("class", function(d, i) {
return "network-node";
})
.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
).merge(forceNetwork.node);
forceNetwork.nodeEnter
.append("circle")
.attr("id", function(d) {
return d.id;
})
.attr("class", "networkviewer-node")
.attr("fill", "red")
.attr("r", 20);
// Append images
forceNetwork.nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.id });
The problem here is that you'er merging the update and enter selections...
forceNetwork.nodeEnter = forceNetwork.node
.enter()
.merge(forceNetwork.node);
... and, after that, appending the circles (and lines):
forceNetwork.nodeEnter
.append("circle")
That will necessarily duplicate the circles and lines.
Instead of that, merge the selection after appending the circles and lines to the enter selection, and also change the selection in your tick function.
Here is the refactored code: https://codepen.io/anon/pen/GXYozm?editors=1010
My code works and it shows me the elements for my data, but d3 doesn't update the text of my SVG Element after changing my data and running the same code again. I have to refresh the whole site for it to change.
var blackbox= d3.select("#content")
.selectAll(".silencer")
.data([0]);
blackbox.enter()
.append("div")
.attr("class", "silencer")
.attr("id", "silencer");
blackbox.exit().remove();
var box = blackbox.selectAll(".ursa")
.data(fraung);
box.enter()
.append("div")
.attr("class", "ursa")
.each(function(d) {
d3.select(this)
.append("svg")
.attr("class", "invoker")
.each(function(d) {
d3.select(this).append("svg:image")
.attr("xlink:href", "images/qwer.png")
.attr("x", "0")
.attr("y", "0")
.attr("width", "100")
.attr("height", "100")
.append("title")
.text(function(d) {return (d.name)});
});
d3.select(this).append("div")
.attr("class", "chen")
.each(function(d) {
d3.select(this).append("table")
.attr("class", "tab")
.each(function(d) {
d3.select(this).append("tbody")
.each(function(d) {
d3.select(this).append("tr")
.text(function(d) {return("Name: ")})
.attr("class", "key")
.each(function(d) {
d3.select(this).append("td")
.text(function(d) {return (d.name)});
});
});
});
});
});
box.exit().remove();
It sounds like your SVG element isn't being cleared before you load the second data set and so the second data set is being drawn behind the first. If the only thing that is changing is the text, it would look like nothing is happening at all. A browser refresh would clear any dynamically drawn content. Before you invoke "blackbox", you can do something like this to clear everything in the "#content" element, which I'm assuming is your SVG container.
if(!d3.select("#content").empty()) d3.select("#content").remove();
This will remove the SVG container entirely. So you'll have to create it again before you can load in new data. Alternatively if you just want to remove the child elements from the SVG container, you could do this:
if(!d3.select("#content").selectAll("*").empty()) d3.select("#content").selectAll("*").remove();
This is a very basic question, but how do I access the value of attributes in d3?
I just started learning today, so I haven't figured this out yet
Suppose I have this as part of my code here
http://jsfiddle.net/matthewpiatetsky/nCNyE/9/
var node = svg.selectAll("circle.node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function (d) {
if (width < height){
return d.count * width/100;
} else {
return d.count * height/100;
}
})
.on("mouseover", animateFirstStep)
.on("mouseout",animateSecondStep)
.style("fill", function(d,i){return color(i);})
.call(force.drag);
For my animation the circle gets bigger when you mouse over it, and I want the circle to return to its normal size when you move the mouse away. However, i'm not sure how to get the value of the radius.
i set the value here
.attr("r", function (d) {
if (width < height){
return d.count * width/100;
} else {
return d.count * height/100;
}
I tried to do node.r and things like that, but i'm not sure what the correct syntax is
Thanks!
You can access an attribute of a selection with:
var node = svg.selectAll("circle.node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function (d) { return rScale(d.count); })
.on("mouseover", function(d) {
d3.select(this)
.transition()
.duration(1000)
.attr('r', 1.8 * rScale(d.count));
})
.on("mouseout", function(d) {
d3.select(this)
.transition()
.duration(1000)
.attr('r', rScale(d.count));
})
.style("fill", function (d, i) {
return color(i);
})
.call(force.drag);
in this context, this points to the DOM element binded with d. Normally, the area of a circle must be proportional to the quantities that you are showing, take a look at the documentation of Quantitative Scales. A fork of your fiddle is here.