D3 Select <g> Element and Modify Fill - javascript

I have used D3 to make a map of the US and filled in the colors
var map = d3.select("#map").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
d3.json("us.json", function (error, us) {
map.append("g")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.style("stroke", function (d) { return "#000000"; } )
.style("fill", function (d) { return gradient(Math.random()); } )
};
Now, I want to change the color of each state but rather than removing the map and then re-adding it, I would like to transition the colors.
I have tried:
d3.selectAll("#map").selectAll("g").selectAll("path")
But then trying to loop through the elements of this array does not help.
What am I doing wrong?
EDIT:
The code I am using to try and change the colors of each state (each path variable) is...
d3.select(this).transition().style("fill", gradient(Math.random()));
I do not believe the problem has to do with the code above - it's the code I am trying to use to select the paths/states that is giving me trouble.
I have also tried
d3.selectAll("path").attr("fill", function (d) { ... });
But, that too, did not do anything. :(

As Lars said in the comments, since I used "style" before, I have to do it again [instead of using attr as I was before].
Selecting the data as I did (d3.selectAll("path")) is the correct way to select the states.

Related

d3.js: Multiple Series Line Chart

With this code as a starting point: https://bl.ocks.org/d3noob/15e4f2a49d0d25468e76ab6717cd95e7 I'm attempting to make a simple line graph with multiple data series. Per #wasserholz suggestion, I've added a complete MVP: https://github.com/djmcmath/d3-fail-mvp
The data parsing portion seems to be working correctly, in that I'm creating a map with 5 elements, each of which is a "sailing date" and array of values associated with that date. This is the variable named "group_by_sailing_date".
The axes appear to be reasonable: For the X-axis, I'm taking the "since_midnight" value, pulling the extents, and formatting it as a time. The Y-axis, similarly, is just the extents of the "margin" value. I get this -- so far so good:
Next, I want to add some lines to my chart. My thinking is that I iterate through the map, and for each of the map elements, I add the element as data to a series. What I get is a gigantic "Nothing Happens" though. No lines, no errors, just "Gosh, your data looks great, but I'm going to ignore it."
//line generator?
var valueline = d3.line()
.x(function(d) { return x(d.since_midnight); })
.y(function(d) { return y(d.margin); });
group_by_sailing_date.forEach(function (s) {
svg_summ.append("path")
.data(s)
.attr("stroke", "steelblue")
.attr("stroke-width", "3px")
.attr("d", valueline);
});
I feel like I'm missing something really fundamental here, but I'm drawing a complete blank (pun intended, ha ha ha). Help?
Following the rules of selection within d3.js (https://bost.ocks.org/mike/selection/) the code should look something like this:
svg_summ.selectAll("path")
.data(group_by_sailing_date)
.enter()
.append("path")
.attr("stroke", "steelblue")
.attr("fill", "none")
.attr("stroke-width", "1px")
.attr("d", (d) => valueline(d));
Ok, I've made it work, but I don't know why. I removed the .data(s) line, and changed "valueline" to "valueline(s)". No idea why this works. Technically, it's all good, but I'd be thrilled if someone could help me understand what this code actually means.
group_by_sailing_date.forEach(function (s) {
svg_summ.append("path")
.attr("stroke", "steelblue")
.attr("fill", "none")
.attr("stroke-width", "1px")
.attr("d", valueline(s));
});

D3.js pass variable into function

I am trying to modify code that updates a line graph every second.
The function takes a variable named path and uses it to make an animation on screen as though the line graph is moving.
Here is a code snippet. (The entire code, working example, and citation is linked at the end of this question.)
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
tick();
function tick() {
// push a new data point onto the back
data.push(random());
// redraw the line, and slide it to the left
path
.attr("d", line)
.attr("transform", null)
.transition()
.duration(500)
.ease("linear")
.attr("transform", "translate(" + x(-1) + ",0)")
.each("end", tick);
// pop the old data point off the front
data.shift();
}
What I am trying to do is change the function so that it takes the path variable as an argument, so that I can use the same function (tick) for modifying various different paths.
However, when I change the above code to something like this:
tick(path);
function tick(path) { /*same as before*/ }
I get the TypeError path.attr is not a function.
And, if instead, I try to do something like this:
tick(d3.select(path));
function tick(path) { /*same as before*/ }
I get the error: cannot read property 'length' of undefined.
Can someone help me out?
Working example: Scroll down to second graph: http://bost.ocks.org/mike/path/
Working code: https://gist.githubusercontent.com/mbostock/1642874/raw/692ec5980f12f4b0cb38c43b41045fe2fe8e3b9e/index.html
Citation for code, example: mbostock on Github
The error is not caused by the first call to tick(path) but by subsequent calls when tick() gets called as a callback you registered by .each("end", tick);. These calls will not pass the required parameter path to your function. And, looking at D3's source I don't see a way of passing this parameter to the callback.
You can circumvent these issues by parameterising your callback function with the path you want it to act on:
tick(path)();
function tick(path) {
var paramTick = function() {
// push a new data point onto the back
path.datum().push(random());
// redraw the line, and slide it to the left
path
.attr("d", line)
.attr("transform", null)
.transition()
.duration(500)
.ease("linear")
.attr("transform", "translate(" + x(-1) + ",0)")
.each("end", paramTick);
// pop the old data point off the front
path.datum().shift();
};
return paramTick;
}
This one uses a closure to save the reference to the path for future calls. The function tick(path) defines an inner function paramTick() which is basically the function you used previously. It does, however, save the reference to the parameter path by means of a closure. Notice, that it passes the reference to itself as the callback parameter for the end event instead of tick(). This way you'll end up with a callback which gets parameterised by the call
tick(path)();
The call to tick(path) returns the parameterised version of your rendering logic which is immediately executed by the second set of parentheses.
I set up a working JSFiddle moving two paths to demonstrate this.
Although this does answer your question, it doesn't seem to be a perfect solution for the problem. Looking at the example moving two paths you will notice that they will get slightly out of sync every now and then. This is caused by utilizing different transitions for each path, which will not get perfectly synchronized.
In my opinion the better approch would be to group multiple paths into one g and apply the transition to this group. With only slight modifications to your original code (JSFiddle v2) this will give a smooth, synchronized movement of all paths.
var data = [
d3.range(n).map(random),
d3.range(n).map(random)
];
var paths = group.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("class", function(d,i) {
return "line" + i;
})
.attr("d", line);
tick();
function tick() {
// push a new data point onto the back
paths.each(function(d) {d.push(random());});
// redraw the line
paths
.attr("d", line);
// Apply the transition to the group
group
.attr("transform", null)
.transition()
.duration(500)
.ease("linear")
.attr("transform", "translate(" + x(-1) + ",0)")
.each("end", tick);
// pop the old data point off the front
paths.each(function(d) {d.shift();});
}

Why would a string of methods need to be separated?

I'm implementing a reactive line chart in meteor.js based on this example line chart. In the code that I lifted for that chart, I have the following block, which works fine.
var paths = svg.selectAll("path.line")
.data([dataset]);
paths
.enter()
.append("path")
.attr("class", "line")
.attr('d', line);
paths
.attr('d', line);
paths
.exit()
.remove();
However, when I try writing something like the following, the axes still show, but the path does not render. Why the heck could that be?
var paths = svg.selectAll("path.line")
.data([dataset])
.enter()
.append("path")
.attr("class", "line")
.attr('d', line)
.exit()
.remove();
It's because you're calling the functions on different objects. D3 returns update, enter, and exit selections from calls to .data() -- this is what you're storing in paths in the first code block. Then you get the enter, update, and exit selections and handle them.
In the second code block, you're calling .enter() you're handling the enter selection afterwards. That is, all the code after the .enter() is being applied to the enter selection and not to the other selections as well as before.
So the .exit().remove() is being called on the newly-appended path elements (which should give you an error) instead of the return value of .data() as in the first block of code.

Updating a pie chart with a new data set with more values

I'm relatively new to D3 and have been following a few pie chart tutorials.
Namely the Mike Bostock Tutorials. http://bl.ocks.org/mbostock/1346410
But I have question about a donut chart updating from one data set to another with the 2nd data set having much more values than the first.
I have attempted this numerous times through an update function but no luck, I'll keep it simple and give a hypothetical example , lets say my first data set had 5 values
[1,2,3,4,5]
and my second data set had 10 values
[1,2,3,4,5,6,7,8,9,10]
only 5 values of the new data set would be depicted on the arcs after the dynamic update. It's like the pie is fixed with only 5 arc sections being able to display 5 values of the new dataset.
Any help would be appreciated as its been stumbling around with the idea for awhile!
The key to making it work with data of different size is to handle the .enter() and .exit() selections. This tutorial goes into more detail, but briefly the enter selection represents data for which no DOM elements exist (e.g. in the case where you pass in more data), the update selection (which you're already handling) represents data for which DOM elements exist and the exit selection DOM elements for which no data exists anymore (e.g. when you have more elements to start with).
So in your change function, you would need to do something like this.
function change() {
clearTimeout(timeout);
var path = svg.datum(data).selectAll("path")
.data(pie);
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // add the new arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
path.exit().remove(); // remove old arcs
}
This assumes that you're updating your data variable as you suggest above instead of getting a different value from the existing data structure as in the example.
Here I made a simple update that triggers when you click the text above the pie chart: JsFiddle
The main thing happening is all the data is updated when the .on("click") event triggers, so the chart gets updated like so:
d3.select("#update")
.on("click", function (d) {
data = [1,2,3,4,5,6,7,8,9,10];
vis.data([data]);
arc = d3.svg.arc().outerRadius(r);
pie = d3.layout.pie().value(function(d){return d; });
arcs.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.append("svg:path")
.attr("fill", function(d, i){return color(i);}).attr("d", arc);
});

d3: confusion about selectAll() when creating new elements

I am new to d3 and am using 'Interactive Data Visualization for the Web' by Scott Murray (which is great btw) to get me started. Now everything I saw so far works as described but something got me confused when looking at the procedure to create a new element. Simple example (from Scott Murray):
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
The name "circle" is used for the selectAll which returns an empty selection (which is ok as I learned). Then circles are appended by putting the same name into the .append. Great!
Now what got me confused was what happens when you want to do the same thing again. So you have a second dataset and want to generate new circles in the same way. Using the same code just replacing the dataset will obviously not work as the selectAll("circle") will not return an empty selection anymore. So I played around and found out that I can use any name in the selectAll and even leave it empty like this: selectAll()
Scott Murrays examples always just use one type (circle, text, etc.) per dataset. Finally I found in the official examples something like
svg.selectAll("line.left")
.data(dataset)
.enter()
.append("line")
.attr ...
svg.selectAll("line.right")
.data(dataset)
.enter()
.append("line");
.attr ...
Now my question: How is this entry in selectAll("ENTRY") really used? Can it be utilized later to again reference those elements in any way or is it really just a dummy name which can be chosen in any way and just needs to return an empty selection? I could not find this entry anywhere in the resulting DOM or object structure anymore.
Thank you for de-confusing me.
What you put in the selectAll() call before the call to .data() really only matters if you're changing/updating what's displayed. Imagine that you have a number of circles already and you want to change their positions. The coordinates are determined by the data, so initially you would do something like
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
Now your new data has the same number of elements, but different coordinates. To update the circle positions, all you need to do is
svg.selectAll("circle")
.data(newData)
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
What happens is that D3 matches the elements in newData to the existing circles (what you selected in selectAll). This way you don't need to append the circles again (they are there already after all), but only update their coordinates.
Note that in the first call, you didn't technically need to select circles. It is good practice to do so however just to make clear what you're trying to do and to avoid issues with accidentally selecting other elements.
You can find more on this update pattern here for example.

Categories

Resources