I am creating a line chart with D3.js. The line should appear over time while new data points are calculated 'on the fly' (i.e. the data array constantly grows). I have the function that does the data calculation, as well as the updateLine() function you see below, in a setInterval(). My problem is, that this function creates a new svg path for every newly added data point, resulting in a huge number of <path> elements.
function updateLine() {
canvas
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
How can I 'extend' the existing path with the new data points?
I found an answer:
Clearly, the code above appends a new <path> every time the function is called. To avoid a <path> 'flood' in the DOM, I found two options:
Option 1
Before appending a new path, select the old path, that lacked the new data points, and remove it by calling remove(). In order to avoid selecting the wrong path, I used an ID selector.
function updateLine() {
canvas
.selectAll('#myPath').remove();
canvas
.append('path')
.datum(data)
.attr('id', '#myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
Option 2 (more elegant, I think)
Use a selectAll() to select the path, bind the data to the selection and call a join() on the selection. In the first call of updateLine(), the path is created (no SVG path exists and we have a nonempty enter selection to which a SVG path is appended and all attributes are set). In the following calls, the path exists in the DOM and the newly updated data array is bound to it. Thus the enter selection is empty and the update selection gets relevant, where we update the path with the new data.
function updateLine() {
canvas.selectAll('#myPath')
.data([data])
.join(
function(enter) {
console.log('Enter selection:');
console.log(enter);
return enter
.append('path')
.attr('id', 'myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
},
function(update) {
console.log('Update selection:');
console.log(update);
return update
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
}
);
}
A couple notes regarding the code of option 2:
It is important to use a selectAll() and not just a select() here,
since in the first call of the function, no <path> exists. select() would select the first match which remains empty in this case.
I call data([data]) and thus perform a join of data points in the data array with SVG elements. datum() would, to my understanding, not perform a join, however, this is important here, as we rely on the update selection.
Passing the data array as an array again to data([data]) causes a data bind of all data points to the one path element, which is exactly what we want here.
Related
I have data that can have a variable numbers of series. And inside each of those series is a date and number that I want to plot as a scatter plot in D3js.
This is my (non working) code. It works when I do it straight, but not once I add the $.each loop. I'm pretty sure its some sort of problem with indexing or something like that.
var color = d3.scale.category20();
// Now actually add the data to the graph
$.each(mpgData, function(k, v) {
console.log(v);
//console.log(k);
svg.selectAll('circle')
.data(v)
.enter()
.append('circle')
.attr('cx', function(d, i) {
console.log(i);
//console.log(d);
return xScale(getDate(d[1]));
})
.attr('cy', function(dd, ii) {
//console.log(ii);
return yScale(dd[2]);
})
.attr('fill', function(d, i) {
return color(k);
})
.attr("class", "mpgColorClass"+k)
.attr("r", 5)
.on("mouseover", function() {
d3.selectAll(".mpgColorClass"+k)
.attr("r", 8);
})
.on("mouseout", function() {
d3.selectAll(".mpgColorClass"+k)
.attr("r", 5);
});
});
I only showed what I think is the relevant part.
So that code kind of works. But it only shows 6 things, which I think is because the 2nd 'series' has 6 items. So somehow its not looping over everything at the "attr('cx', function(d, i)) part. I think I'm not understanding how to get that function to loop over each part of the series.
I'm new to D3js, so still struggling through the learning curve. But it works and I get a graph out with the correct data. Its just not ALL the data, only 6 points out of the entire (variable) dataset.
Thanks!
in your $.each() block you are overwriting the same set of circles in the SVG element. So instead of using selectAll('circle') you can do this:
$.each(mpgData, function(k, v) {
svg.selectAll('circle' + k)
.data(v)
.enter()
.append('circle')
.attr('class','circle' + k)
});
truncated rest of details in your code... edit at will.
I'm building a ring chart on last.fm's API, using live data on music listening habits. I want users to be able to enter their username, and the number of most-popular artists they'd like to see. They can then enter another username or another number of artists, and the chart re-draws itself with that new pulled data.
However, right now, I'm seeing that when I pull data that has less data points than the previous set (e.g. I ask for the top 50 artists from user X, and then the top 3 artists from user X), the ring chart ends up being remade with gaps, like so:
And then:
Moving from smaller to larger datasets (e.g. 5 artists to 50 artists) doesn't generate this problem. Here's my transition code:
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
var sum = 0;
for (i=0 ; i<d.values.length ; i++) {
sum += d.values[i].playcount;
}
return sum;
});
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("d", arc);
I'm convinced it has something to do with the exit/update/enter order. When I check each element in the DOM, though, the data seems to be updating and attaching itself to the right elements. I don't understand why it's not filling up to a new full, 360-degree ring chart though. Full JSFiddle here.
It was a simple enough fix, in the end, arrived after some time meditating over Thinking with Joins:
Before (gives gap-toothed ring charts):
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
After (no gaps):
var path = svg.selectAll("path")
.data(pie(dataset));
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white");
path.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
Now the mystery: why do we need to restart path. when dealing with the d elements?
First time when you load the data with 10 items, D3JS bind these items' data to new 10 paths. In the next time, you load new data with 3 items, by default without using key function, first 3 items are updated because there has been 3 paths existed with its d attribute having old values; 7 previous paths are removed. You should add key function or update d attribute as following:
var path = svg.selectAll("path")
.data(pie(dataset));
path.attr("d", arc); // only need to add one line here, other lines aren't changed.
path.exit().remove();
path.enter()
.append("path")
.style("stroke-width", "1px")
.style("stroke", "white")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
Hey it's probably been a really long time but I figured out that whatever you don't separate from enter() and append() doesn't get updated. So if you for example change your stroke width or stroke color you need to move it to the second path. in order for them to get updated. Currently you only update the arc and the fill color.
I had this problem and I was tearing out my hair over it, so thanks for the help!
today I created a simple Bar Chart with the enter-update-exit logic - everything works fine. Now I will do the same with a line chart - the whole dataset for the chart can change at any time, so I will update the chart smoothly. I can't find a good example with a line chart and the enter-update-exit logic (or I'm looking wrong). Currently I have to remember that if the chart is called for the first time or the data to be updated (data has changed) - This is my dirty version:
var channelGroup = d3.select(".ch1");
// init. Line Chart
if (firstLoad) {
channelGroup
.append('path')
.attr("class", "line")
.style("stroke", channel.Color)
.transition()
.ease("linear")
.duration(animChart / 2)
.attr('d', line(channel.DataRows));
}
// update Line Chart
else {
channelGroup
.selectAll("path")
.data([channel.DataRows])
.transition()
.ease("linear")
.duration(animChart / 2)
.attr("d", function (d) { return line(d); });
}
how can I realize this in a good manner?... thx!
You're mixing up two different approaches, one binding data to the line and the other just passing the data to the line function. Either one could work (so long as you only have the one line), but if you want to get rid of your if/else construct you're still going to need to separate the statements handling entering/appending the element from the statements updating it.
Option 1 (don't bind data, just call the function):
var linegraph = channelGroup.select('path');
if ( linegraph.empty() ) {
//handle the entering data
linegraph = channelGroup.append('path')
.attr("class", "line")
.style("stroke", channel.Color);
}
linegraph
.transition()
.ease("linear")
.duration(animChart / 2)
.attr('d', line(channel.DataRows));
Option 2 (use a data join):
var linegraph = channelGroup.selectAll("path")
.data([channel.DataRows]);
linegraph.enter().append('path')
.attr("class", "line");
linegraph
.transition()
.ease("linear")
.duration(animChart / 2)
.attr("d", line); //This is slightly more efficient than
//function (d) { return line(d); }
//and does the exact same thing.
//d3 sees that `line` is a function, and
//therefore calls it with the data as the
//first parameter.
new to d3 but have made it to the stage of loading data from a csv file and visualising it using svg elements:
d3.csv("data.csv", function(data) {
csvData = data;
svg.selectAll("circle")
.data(csvData)
.enter()
.append("circle")
.attr("cx", function(d) { return d.LocationX})
.attr("cy", function(d) { return d.LocationY})
.attr("r", 10)
.attr("stroke", "black")
.attr("fill", "orange")
})
Obviously, all my data points are plotted simultaneously. Is there an efficient way of using D3 functions to "build" the plot point by point? Can't find an example to follow.
You can simulate this by adding the circles with radius 0 and then increasing it with a transition (which you can, but don't have to, animate). The code would look like this.
svg.selectAll("circle")
.data(csvData)
.enter()
.append("circle")
.attr("cx", function(d) { return d.LocationX})
.attr("cy", function(d) { return d.LocationY})
.attr("stroke", "black")
.attr("fill", "orange")
.attr("r", 0)
.transition().duration(0)
.delay(function(d, i) { return i * 500; })
.attr("r", 10);
This creates the circles with radius 0 and then adds a transition with a delay that increases with the index of the circle which increases the radius to 10. That is, initially there will be one circle visible, after 500ms 2, after 1s 3 and so on. You can obviously customise this interval.
To clarify, all the DOM elements will be created at the start, you're only changing the attributes such that it will seem that they are appearing one after the other. The advantage of doing it this way is that it fits nicely with the rest of the D3 functionality and you don't have to resort to anything else to separate your data.
The d3 enter() is adding all of the elements in the csvData array at one time. You can build the graph up one point at a time by starting with an empty array, pushing elements from csvData (i.e. points) into it one at a time then running the
svg.selectAll("circle")
.data(NewArray)
.enter()
.append("circle")
on this new array each time a new point is added.
Here is a working example https://bl.ocks.org/AndrewStaroscik/7371301 using a setInterval that is cleared once all the points are added.
In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).