D3.js pass variable into function - javascript

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

Related

"TypeError: d is undefined" when transitioning on a bar chart with a mouseover event

I was working on adding in transitions to my bar chart where they start from the axis and rise up into the desired height. I got the transitions to work but then the mouseover event no longer worked. I am confused as to why d is undefined if I have already appended the data to the plwdhBar object and am calling it later in the code.
After looking at similar solutions, it seemed that the solution was to call the bar chart bars once to set them up from the data, then to add the transition, then to add in the mouseover event.
Here is what I currently have: (working transition but not working mouseover, results in error for showTooltip(d) function)
var plwdhBar = svg.append("g")
d3.csv("Data/"+name+".csv").then(function(dataset) {
plwdhBar.selectAll("plwdhBar") //bar2
.data(dataset)
.enter()
.append("rect")
.attr("class","plwdhBar")
.attr("width", 30)
.attr("fill", bar2Color)
.attr("x",(d)=>xScale(d.year)+(barW/2)-15)
.attr("y", h-margin.bottom)
.transition()
.duration(200)
.attr("y",(d)=>yBarScale(d.plwdh))
.attr("height", (d)=>(h-yBarScale(d.plwdh)) -margin.bottom)
plwdhBar.on("mouseover",function(d){
showTooltip(d);
}).on("mouseout",function(){
removeTooltip();
});
}
I have also tried taking out the lines of code for the transition and making it:
plwdhBar.transition()
.duration(200)
.attr("y",(d)=>yBarScale(d.plwdh))
.attr("height", (d)=>(h-yBarScale(d.plwdh)) -margin.bottom)
however then that also resulted in an undefined error for d
The error in all the situations is TypeError: d is undefined and occurs in the first code snippet when then removeToolTip(d) function is called and in the scenario where the second code snippet is implemented the error occurs on the .attr("y",(d)=>yBarScale(d.plwdh)) line.
This is your plwdhBar variable:
var plwdhBar = svg.append("g");
It's just this, nothing more. No data bound to it. Keep this in mind because this information is important for understanding what's wrong here.
Then you do:
plwdhBar.selectAll("plwdhBar")
.data(dataset)
.enter()
.append("rect")
//etc...
This, of course, is appending your rectangles, and if you added the listeners in that chain it would work. But pay attention to the fact that this does not change what plwdhBar is, it keeps being just var plwdhBar = svg.append("g");, as we showed initially.
Therefore, when you later do...
plwdhBar.on("mouseover",function(d){
showTooltip(d);
})
... you get the error because, of course, there is no data here.
Solution: name your selections!
For instance:
var foo = plwdhBar.selectAll("plwdhBar")
.data(dataset)
.enter()
.append("rect")
//etc...
Now you can do:
foo.on("mouseover",function(d){
showTooltip(d);
})
However, there is an important advice: transitions also have a method named on. So, remove the transition from the main chain, otherwise you'll get another error (not related to this one). You can add the transition separately:
foo.transition()
//etc...

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.

D3 Select <g> Element and Modify Fill

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.

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

Map does not render completely in d3.js

I am creating a globe on which I plot a number of locations. Each location is identified with a small circle and provides information via a tooltip when the cursor hovers over the circle.
My problem is the global map renders incompletely most of the time. That is various countries do not show up and the behavior of the code changes completely at this point. I say most of the time because about every 5th time i refresh the browser it does render completely. I feel like I either have a hole in my code or the JSON file has a syntax problem that confuses the browser.
btw: I have the same problem is FF, Safari, and Chrome. I am using v3 of D3.js
Here is the rendering code:
d3.json("d/world-countries.json", function (error, collection) {
map.selectAll("path")
.data(collection.features)
.enter()
.append("svg:path")
.attr("class", "country")
.attr("d", path)
.append("svg:title")
.text( function(d) {
return d.properties.name; });
});
track = "countries";
d3.json("d/quakes.json", function (error, collection) {
map.selectAll("quakes")
.data(collection.features)
.enter()
.append("svg:path")
.attr("r", function (d) {
return impactSize(d.properties.mag);
})
.attr("cx", function (d) {
return projection(d.geometry.coordinates)[0];
})
.attr("cy", function (d) {
return projection(d.geometry.coordinates)[1];
})
.attr("class", "quake")
.on("mouseover", nodehi)
.on("mouseout", nodelo)
.attr("d", path)
.append("svg:title")
.text( function(d) {
var tip = d.properties.description + " long "+ (d.geometry.coordinates)[0] + " lat " + (d.geometry.coordinates)[1];
return tip
});
});
Any thoughts would be appreciated...
The reason you're seeing this behaviour is that you're doing two asynchronous calls (to d3.json) that are not independent of each other because of the way you're selecting elements and binding data to them. By the nature of asynchronous calls, you can't tell which one will finish first, and depending on which one does, you see either the correct or incorrect behaviour.
In both handler functions, you're appending path elements. In the first one (for the world file), you're also selecting path elements to bind data to them. If the other call finished first, there will be path elements on the page. These will be matched to the data that you pass to .data(), and hence the .enter() selection won't contain all the elements you're expecting. This is not a problem if the calls finish the other way because you're selecting quake elements in the other handler.
There are several ways to fix this. You could either assign identifying classes to all your paths (which you're doing already) and change the selectors accordingly -- in the first handler, do .selectAll("path.country") and in the second .selectAll("path.quake").
Alternatively, you could nest the two calls to d3.json such that the second one is only made once the first one is finished. I would do both of those to be sure when elements are drawn, but for performance reasons you may still want to make the two calls at the same time.

Categories

Resources