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!
Related
This question has been asked before but I don't think the given solution is the cleanest way to do it so I'm hoping someone may have figured it out since then. I am generating multiple pie charts using d3.js and am dynamically updating them through SQL queries. This is my update function:
function updateCharts()
{
var updatedDataSet = getDataSet();
// Create a pie layout and bind the new data to it
var layout = d3.layout.pie()
.value(function(d, i) { return d[i].count; })
.sort(null);
// Select the pie chart
var pieChartSVG = d3.selectAll("#pie");
// Select each slice of the pie chart
var arcsUpdate = pieChartSVG.selectAll("g.slice")
.data(layout([updatedDataSet]))
.enter();
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.select("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
}
But it doesn't work. If I take the .enter() out of arcsUpdate then it works but applies the same changes(data and tweens) to each chart. I could get around this by doing a foreach() on the elements returned from pieChartSVG but I can't think of a way of doing that other than the one described in the other question.
I have had to use the solution from the other question as I have to move forward but it's not a "clean" solution so I'd love to know if anybody is aware of a better way to handle it.
I thought you need take the .enter() out of arcsUpdate just like
var arcsUpdate = pieChartSVG.selectAll("path")
.data(layout([updatedDataSet]));
// Apply transitions to the pie chart to reflect the new dataset
arcsUpdate.enter()
.append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.transition()
.duration(1000)
.attrTween("d", arcTween);
This is the correct way.
And if it applies the same changes(data and tweens) to each chart. Please check out they are binding same updateDataSet or not.
The effect I'm going for in a map based visualisation is:
Red circles on a map represent data (things at locations)
When something happens at a location, we briefly (2 seconds) show another, blue circle fading out over the top.
I'm trying to work out how to do this in the D3 paradigm. Specifically: how do you represent the same thing twice?
The problem I run into is that when I try to add the same dataset twice to a given SVG canvas group, nothing gets added. That is, using code like this:
g = svg.append("g");
var feature = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes; });
var emphasis = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes; });
This workaround is ok, but kludgy and potentially limiting:
g2 = svg.append("g");
var emphasis = g2.selectAll("circle")
That is, adding the second group of elements to a different SVG group.
The proper way to do this is to use classes to select the circles (and applying that class when you create them). So you create the features like so:
var feature = g.selectAll("circle.feature")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "feature") // <- assign the class
....
Similarly, for the emphasis:
var feature = g.selectAll("circle.emphasis")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "emphasis") // <- assign the class
....
I've finally (sort of) figured it out. The two sets of data are treated as one because they share the same key, according to the rules of D3 constancy. So an easy way around is to give each set a key that can't overlap:
var feature = g.selectAll("circle")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes * 1; });
var emphasis = g.selectAll("notathing")
.data(stations, function (d) { return d.id + " cannot possibly overlap"; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes * 1; });
The only slight quirk is I have to modify the second selector (g.selectAll("notathing")) so it doesn't match any of the circles created by the first one.
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.
So I'm using code based off of this..
http://bl.ocks.org/mbostock/3884955
Essentially, what I'm trying to do is at every data point I want to add a circle. Any help would be much appreciated seeing that I have no idea where to start.
This is my code so far: It worked when I was using single lines.
var circlegroup = focus.append("g")
circlegroup.attr("clip-path", "url(#clip)")
circlegroup.selectAll('.dot')
.data(data)
.enter().append("circle")
.attr('class', 'dot')
.attr("cx",function(d){ return x(d.date);})
.attr("cy", function(d){ return y(d.price);})
.attr("r", function(d){ return 4;})
.on('mouseover', function(d){ d3.select(this).attr('r', 8)})
.on('mouseout', function(d){ d3.select(this).attr('r', 4)});
You need nested selections for this. Assuming that data is a two-dimensional array, you would do something like this.
var groups = svg.selectAll("g").data(data).enter().append("g");
groups.data(function(d) { return d; })
.enter()
.append("circle")
// set attributes