D3 pie chart transition - javascript

I'm studying transitions in D3js, trying to get them working with a simple pie chart that is derived from a Mike Bostock example.
I want to transition the data in the pie from data to data2, but the chart does not update.
This is the transition statement:
path.selectAll("path").data(pie(data2)).transition().duration(2000);
What am I missing?
EDIT
I've got it working with the below. Now I'd like to understand why this is working. I understand the .attr("d",arc) part, but why do I need to selectAll("path")?
path.selectAll("path").data(pie(data2)).transition().duration(2000).attr("d",arc);
END EDIT
The complete code (JSFiddle here):
var width = 200,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var data = [2, 19, 18, 99, 100];
var data2 = [100, 1200, 20, 88, 12];
var pie, arc, svg, path, data;
var chartCanvas = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var path = chartCanvas
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function setupPieChart() {
pie = d3.layout.pie()
.value(function (d) {
return d;
})
.sort(null);
arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20)
path
.selectAll("path")
.data(pie(data))
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d;
}); // store the initial angles
console.log(path);
}
setupPieChart();
console.log(path);
path.selectAll("path").data(pie(data2)).transition().duration(2000);

.transition starts a transition for the attributes change(s) declared after it. You don't set / do anything after .transition, so there is nothing to interpolate over. In the original example from Mike, you'll see he sets the d attribute after starting the transition, specifying a custom interpolator (arcTween)

I immediately see you are missing some important part of the update process. You copied the original code, but you forgot the update part :-). I can tell, because I see in your code you store the initial angles.
Look again at the code here and try to understand the function arcTween. More information can be found here. You need an arc tween function to calculate the new angles based on the initial angles (which is why you stored the initial angles in the first place :-).
I won't do the fiddle at the moment, cause in my experience, you learn more if you try to understand the arc tween function (as I did here . This is a link to a personal project of mine, but feel free to copy code as you see fit).
You need to .selectAll("path") as those are the actual elements that will update. When doing d3, try to think of the chart elements as following: Elements that are not visible yet (which is enter collection), elements that are visible now (which can be seen as the update collection) and elements that can be removed (the exit collection).
You need to see those elements based on the data you want to visualize. D3 is data driven documents, so everything is in relation to the data you want to show on the screen: if you have data but no elements yet, you do an "enter" of elements. So you do a selection of elements that are not in the DOM yet, but will soon be, because you will bind them to the data you have.
If you already have elements on the screen, and the number of elements matches the data you need to show (for example: var data = [20 ,30 , 40], you got 3 pieces of data here for 3 div's on the screen, you got a matching amount), then d3 will update your selection (hence the update collection or update selection) so the elements properties match the data.
If you have more elements on the screen then there is data to show, then you can do an exit of elements (again, hence the exit selection or collection). I hope that made it a bit more clear and that it made sense as well :-)

Related

d3.geoPath().projection is rendering black rectangle instead of map

I want to create a US map based on county data. I'm using this JSON topology data to create the graph: https://cdn.freecodecamp.org/testable-projects-fcc/data/choropleth_map/counties.json
In the first step, I created the map like this, and it works fine:
var path = d3.geoPath();
svgContainer.selectAll("path")
.data(topojson.feature(countyData, countyData.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
Picture: US map renders OK but too large
However, it is too large for my purpose so I'm trying to scale it down. I tried projections which I saw in several other projects (for example here: https://www.d3-graph-gallery.com/graph/choropleth_basic.html). Unfortunately it just renders a black rectangle. I also tried geoAlbersUsa() and some other projections, but it did not help. How can I get the map data to scale?
var projection = d3.geoAlbersUsa() // geoMercator() also does not work
.scale(200)
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
svgContainer.selectAll("path")
.data(topojson.feature(countyData, countyData.objects.counties).features)
.enter()
.append("path")
.attr("d", path)
Picture: projection renders black rectangle
What am I doing wrong here?
Everything looks good in the second block of code (using d3.geoAlbersUSA()) except I think you are zoomed in too close with .scale(200) and only seeing the middle of a county. As explained in this post, if you zoom out with smaller scale value you may start to see more of your map.(What does it mean to scale a projection in d3?)
You may be better off using .fitSize() instead of .scale since you seem to be trying to fit the whole topojson data set inside an area rather than zooming into part of it. Updated your example below using a variable margin.
var margin = 20; //amount of whitespace you want around the map
var projection = d3.geoAlbersUsa()
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
var countiesFeatureCollection = topojson.feature(countyData, countyData.objects.counties);
//make the map projection fit into size of screen minus margin on all sides
projection.fitSize([width - margin*2, height - margin*2], countiesFeatureCollection);
svgContainer.selectAll("path")
.data(countiesFeatureCollection.features)
.enter()
.append("path")
.attr("d", path)

Is there a way to add different shapes depending on different data to my force-directed graph?

I'm sure this has been covered in another question on here however, after trying many different examples on my code i cannot seem to get this to work.
I am trying to make the nodes on my force-directed graph a different shape depending on the name of the node, for example, if the node is named 'Switch' it should be displayed as a square.
I have worked out the set up for changing the colour of the nodes based on the data and would like a similar thing for the shape but can't get it to work using d3.v4.
Any help?
var color = d3.scaleOrdinal(d3.schemeCategory10);
var shape = d3.symbolTypes;
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 15)
.attr("fill", function(d) { return color(d.group); })
.attr("d", d3.symbol()
.type(function (d) { return shape(d.name);}))
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
Many Thanks
Faye
You can't use:
var shape = d3.symbolTypes;
....
shape(d.name);
since d3.symbolTypes is not defined in d3 v4 (it was in some versions of v3), in v4 use d3.symbols which is an array not a function.
But, as with color, you could create an ordinal scale for shape:
var color = d3.scaleOrdinal(d3.schemeCategory20);
var shape = d3.scaleOrdinal(d3.symbols);
Now all you have to do is append that shape:
.append('path')
.attr("d", d3.symbol().type( function(d) { return shape(d[property]);} ) );
Since you were appending circles, and are now appending paths, you'll need to change .append('circle'), and as circles have cx cy elements, you need to change to a transform where you set their position.
Here is a bl.ock which should show this in practice, based on MBostock's force directed graph (here)
Keep in mind there are only seven shapes in the d3.symbols array.
Edit:
If you want to specify which shapes get displayed for each node based on a property (rather than letting the ordinal scale set the shape), you could add a property to your data which contains the name of a shape (eg: d3.symbolCross), or create a function which takes in a data value and outputs the name of a symbol. But an ordinal scale is easiest.

D3.js not updating data

I am updating a D3 stacked bar graph in combination with AngularJS. In this, all data is initially visible, and updates filter out undesired data. In the past it has worked without issue using this model:
data = [{
name: John Doe,
splits: [{distance: 1, time: 1234},]
},...]
Now I am attempting to to add one more bar to each stack using this model:
data = [{
name: John Doe
time: 12345,
splits: [{distance: 1, time: 1234},]
},...]
My issue is updating the data. My calculated values are recalculated correctly, such as the domain for scaling. A line for time update still only recognizes the data values from before the update (code snippet heavily truncated for brevity):
// Set ranges of values in axes
x.domain(data.map(function(d) { return d.name}));
y.domain([ min , max]);
// Y Axis is updated to the correct time range
chart.append('g').attr('class', 'y axis').call(yAxis).selectAll('text').style('font-family','Open Sans').style('font-size', '.9rem');
// data join / Select Athlete
var athlete = chart.selectAll(".athlete").data(data),
athdata = athlete.enter(),
console.log(data) // data is correct here
// add container for each athlete
athplace = athdata.append("g")
.attr("class", "athlete")
.attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; })
.text(function(d) { return d.name}),
// ENTER time
athplace.append('rect')
.attr('class', "time")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", 0).transition().duration(300)
.attr("height", function(d) { return height-y(d.time); });
... enter splits & labels
// exit splits & athlete
splitlabels.exit().transition().duration(300).attr('opacity', 0).remove();
splits.exit().transition().duration(300).attr('height', 0).attr('opacity', 0).remove();
athlete.exit().transition().duration(300).attr('width',0).attr('opacity', 0).remove();
console.log(athlete) // data is still correct
// UPDATE time, this chain has the problem with data not being updated. "d" is equal to previous values
athlete.selectAll('rect.time')
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", function(d) { return height-y(d.time); });
Due to these errors, the updated columns represent the wrong data and produce the wrong visualization. I have been staring/testing at this all day trying to isolate the issue to what I have now. Could someone more experienced with D3 give me some insight?
Note: for those interested, this is all in an Angular directive where I $watch for changes to the data values, although I am 100% sure that that is not the issue.
Edit
Here is a JSFiddle that illustrates the same error as in my script. All of the code was extracted directly from the script exhibiting the issue. The interval update at the bottom imitates the swapping of data that would normally happen.
I played with your example for a little bit and made a few changes that may be of use.
First, I re-factored your global var's up top to inside your update function. By doing this, we are not "double-appending" where our x and y axis left off. This was seemingly drawing our chart on top of our old chart.
However, fixing this then gave me the new chart overlaying on top of our old. To fix this, I call d3.selectAll("svg > *").remove(); within update to remove all attached SVG elements and groupings, giving us a "clean slate" to render our new chart on. This solves the overlaying issue.
You mention this is shortened for brevity, so I hope this example can help with your actual application
Updated Fiddle

Improving D3 Sequence Sunburst Example

This D3 example served as my starting point:
http://bl.ocks.org/kerryrodden/7090426
I wanted to change data that feeds the diagram, and I made following new example:
http://jsfiddle.net/ZGVK3/
One can notice at least two problems:
Legend is wrong. This is because it still contains 'hardcoded' names from original example.
All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.
How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?
You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.
Step 1. Create an ordinal scale for the colors
Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:
var colors = d3.scale.ordinal()
.range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);
This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.
As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.
Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].
Step 2. Use your data to construct the domain of the scale
The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) {
output.push(d.name);
}
});
return output;
})(nodes);
This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.
Then you can simply set the new array to be the domain of the color scale:
colors.domain(uniqueNames);
Step 3. Use the scale's domain to build the legend
Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.
You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", colors.domain().length * (li.h + li.s));
var g = legend.selectAll("g")
.data(colors.domain())
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors(d); });
And that's about it. Hope that helps.
Here's the updated JSFiddle.

Controlling d3js pie animations and place labels inside pie

I'm a complete noop to D3 and partly SVG, so I got a few basic questions.
First off, my code in question can be viewed at http://dotnetcarpenter.github.io/d3-test/ and I've used Simple Pie Chart example with D3.js and Pie Chart Update, II as examples to get a running start.
As you can see, the animation gets skewed in the end when the low path values switch to the higher values. This is obviously not what I want. I think I'm getting the order of calculations wrong but I'm not sure what to do. I'm using the code from the last example:
function change() {
//...
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
// where arcTween is
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Another issue is placing labels on the sectors. I've put the update stuff in the change function and is able to read out and only render them if the value is between 0 and 100. I can't however place them in any way. Looking at the first example, I figure that I could do something like this:
text.data(data)
.text(setText)
.attr("transform", function (d) {
// we have to make sure to set these before calling arc.centroid
d.innerRadius = 0;
d.outerRadius = radius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
Where text is a d3 selection and arc is: d3.svg.arc().outerRadius(radius)
But I get "Unexpected value translate(NaN,NaN) parsing transform attribute." warning in Firefox and the labels are written on top of each other.
I appreciate any help and hints. Thanks!
I finally figured it out.
Maintain sector order throughout an animation.
You'd be forgiven for thinking that object contancy had something do with it. I did. But it turns out to be much simpler than that.
Every pie chart is by default sorted by value. If you don't want to sort by value but e.g. by data list order, you just have to disable sorting.
var pie = d3.layout.pie() // get a pie object structure
.value(function(d) { // define how to get your data value
return d.value; // (based on your data set)
})
.sort(null); // disable sort-by-value
Positioning labels according to your chart
Basically, you need to calculate your label positions depending on the type of chart or graph, your trying to connect them to. In my case, it's a pie chart. So if I want d3 to help with the calculations, I need to tell centroid the inner and outer radius and, most importantly to my issue, the start and end angles. The latter was missing from my code. Getting these values is as simple as, calling our pie layout above with our dataset and then do a transform.
Note that you don't have to call .data() again if you created the SVG with d3 and already supplied your data wrapped in .pie() structure. That is, that you didn't select any existing SVG from your page.
var svg = d3.select("svg")
// do stuff with your svg
var pie = d3.layout.pie()
// set stuff on your layout
var text = svg.selectAll("text")
.data(pie(dataset)) // where dataset contains your data
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
});
I have to give credit to Philip Pedruco for helping me along the way.
Bonus info
Use viewBox if you want to position your SVG cross browser, not transform/translate.
// move pie to center
d3.select("svg").attr("viewBox", -radius + ","+ -radius +"," + size + "," + size)

Categories