the pie chart update example on the bl.ocks site doesn't update the elements 'in place':
http://bl.ocks.org/j0hnsmith/5591116
function change() {
clearTimeout(timeout);
path = path.data(pie(dataset[this.value])); // update the data
// set the start and end angles to Math.PI * 2 so we can transition
// anticlockwise to the actual values later
path.enter().append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc(enterAntiClockwise))
.each(function (d) {
this._current = {
data: d.data,
value: d.value,
startAngle: enterAntiClockwise.startAngle,
endAngle: enterAntiClockwise.endAngle
};
}); // store the initial values
path.exit()
.transition()
.duration(750)
.attrTween('d', arcTweenOut)
.remove() // now remove the exiting arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Instead, it just treats the new array of value as brand new data and resizes the chart accordingly.
I've created a fiddle demonstrating the issue very simply:
http://jsfiddle.net/u9GBq/23/
If you press 'add', it add a random int to the array: this works as intended.
If you press 'remove', the only element getting transitioned out is always the last element to have entered the pie. In short, it behaves like a LIFO stack.
The expected behaviour is for the relevant pie arc to get transitioned out instead.
Is it possible to apply object consistency to pies? I've also tried adding a key function (not demonstrated on the fiddle) but that just breaks (oddly enough it works fine with my stacked graphs).
Thank you.
The easiest solution to this problem is to set missing values to zero, rather than removing them entirely, as in Part III of the Pie Chart Update series of examples. Then you get object constancy for free: you have the same number of elements, in the same order, across updates.
Alternatively, if you want a data join as in Part IV, you have to tell D3 where the entering arcs should enter from, and where the exiting arcs should exit to. A reasonable strategy is to find the closest neighboring arc from the opposite data: for a given entering arc, find the closest neighboring arc in the old data (pre-transition); likewise for a given exiting arc, find the closest neighboring arc in the new data (post-transition).
To continue the example, say you’re showing sales of apples in different regions, and want to switch to show oranges. You could use the following key function to maintain object constancy:
function key(d) {
return d.data.region;
}
(This assumes you’re using d3.layout.pie, which wraps your original data and exposes it as d.data.)
Now say when you transition to oranges, you have the following old data and new data:
var data0 = path.data(), // retrieve the old data
data1 = pie(region.values); // compute the new data
For each entering arc at index i (where d is data1[i]), you can step sequentially through preceding data in data1, and see if you can find a match in data0:
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j]; // a match!
}
}
If you find a match, your entering arcs can start from the matching arc’s end angle. If you don’t find a preceding match, you can then look for a following matching arc instead. If there are no matches, then there’s no overlap between the two datasets, so you might enter the arcs from angle 0°, or do a crossfade. You can likewise apply this technique to exiting arcs.
Putting it all together, here’s Part V:
Ok, found the solution.
The trick was to pass the key this way:
path = path.data(pie(dataset), function (d) {return d.data}); // this is good
as opposed to not passing it, or passing it the wrong way:
path = path.data(pie(dataset, function (d) {return d.data})); // this is bad
And here's an updated fiddle with a working transition on the right arc! :)
http://jsfiddle.net/StephanTual/PA7WD/1/
Related
I have made a compound bar chart representing footballers within football teams. The chart is here: http://andybarefoot.com/football/path.html
I used d3 and built the page to work in two stages. Firstly I load the data and create a rectangle for each player. I then update the parameters of the rectangles based on the data assigned to each element depending on which view is chosen. This means that the different navigation options resize and rearrange the rectangles based on existing data mapped to the elements but no additional data is loaded in.
Whilst the resizing of the rectangles works correctly I am unable to reorder the rectangles based on the data.
The vertical position of each rectangle is set simply by "i" multiplied by a set spacing variable. To change the order I thought I could selectAll all elements, sort based on the relevant data, and then set the new vertical position in the same way. (i.e. the value of "i" would have changed). However I can't get this to work.
Here is my (unsuccessful) attempt:
// select all elements and then reorder
svg
.selectAll(".team")
.sort(function(a, b) {
return b.totalClubContractDistance - a.totalClubContractDistance;
})
;
// select all elements and reposition according to new order
svg
.selectAll(".team")
.duration(750)
.attr("transform", function(d,i) {
return "translate(0,"+teamSpacing*i+")";
})
;
In d3 there are 4 core concepts. Join, Update, Enter, Exit. You can read more here: https://bost.ocks.org/mike/join/
Basically, every time you want to update the position of an element, you should change the data, then do a join followed by an update.
So the code would look like this:
function render (data) {
// join
// this joins the new data to the existing data
var teams = svg.selectAll('.team')
.data(data);
// update
// this will update existing teams that have a different location
teams.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// enter
// this will add new teams that were added to the data set
teams.enter()
.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// exit
// this will remove all the teams that are no longer part of the data set
teams.exit()
.remove();
}
Hope this helps
first of all:
d3-version: "version": "3.5.17"
i have two differnet kinds of thematic maps, a proportional symbol map and a pseudo-demers cartogram with symbols (circles, same as in proportional symbol) as close to its origin as possible with collision detection and gravity.
However, i want to animate the circles from the proportional symbol map to the cartogram. In particular, i just want to trigger my force-directed graph for collision detection and gravity; this works fine for the first time. I provide an animation back from a cartogram to a proportional symbol map where each symbol just moves back to its centroid. Now, if i want to go back to the cartogram, the code fails and it says Uncaught TypeError: Cannot read property '1' of undefined, even though i trigger it he same time again as the first time. Is this some kind of bug in reusability of the force? or some kind of error on my side?
The problem can be tested here: https://particles-masterthesis.github.io/aggregation/; on the top left, you can switch between proportional symbol and cartogram.
the appropriate code i am using is the following:
case 'psm_cartogram':
let psm = this.currentViz;
information = {
data: psm.nodes,
symbols: psm.symbols
};
upcomingViz.obj = this.canvas.drawCartogram(
null,
this.currentViz.constructor.name === "Cartogram",
information,
() => {
this.currentViz.hide(false, true);
this.fadeOutParticles();
}
);
upcomingViz.type = 'cartogram';
resolve(upcomingViz);
break;
The key part is in the information object, where i share the same objects between the cartogram and the proportional symbol map
in the cartogram, i have the following important code:
```
this.force = this.baseMap._d3.layout.force()
.charge(0)
.gravity(0)
.size([this.width - this.symbolPadding, this.height - this.symbolPadding]);
this.nodes = keepInformation.data;
this.node = keepInformation.symbols;
this.force
.nodes(this.nodes)
.on("tick", this.tick.bind(this, 0.0099))
.start();
...
tick(gravity) {
this.node
.each(this.gravity(gravity))
.each(this.collide(0.25))
.attr("cx", d => { return d.x; })
.attr("cy", d => { return d.y; });
}
gravity(k) {
return d => {
d.x += (d.x0 - d.x) * k;
d.y += (d.y0 - d.y) * k;
};
}
//the collide function is not shown as it is a simple quadtree
```
if it helps in any way, the code is also available at https://github.com/particles-masterthesis/aggregation/src/js/visualization/map The main code is the transition-manager and the two types of maps.
i am thankful for any suggestions and support i can get, even if its a simple hint that i could check out.
PS:
These are two screenshots; the first one is important for the different logs of cartogram:132 which was a console.log(this.node) in the tick-function before doing any gravity etc. and the second one mentions the error.
for more understanding of the first log:
it starts by logging this.node in the tick function; afterwards a visualization change to psm got triggered (cartogram_psm) with a change back to cartogram later on and then the error appeared.
okay so i could figure out my problem:
transition chaining was incorrect in my way and so, the mysterious attributes on the object appeared;
rewriting all transitions with this method helped (https://stackoverflow.com/a/17101823/1472902)
I have some geoJson data that I am charting using d3.geo.
When I write something like
d3.select("svg")
...
.attr("d", function(d) {
return path({
type:"MultiPoint",
coordinates: get_activity_coords_(d.activities)
});
})
I always get a circle for each coordinate. The coordinates represent locations of various stopping points of a journey. What I would prefer is a different shape for the first and the last coordinate.
Is it possible to do this using MultiPoint, is there an example that I can follow? I could draw the points one by one, but I recall reading that MultiPoint is far faster. Plus, the code would be much clearer to read.
Thanks a lot.
You can't do different shapes for MultiPoint geoJSON with d3.geo.path. You can change the radius based on a function, but it looks like you can only set it per feature and not per point, so you'd have to break your set of points into multiple features and lose any performance benefit from using the single element.
However, there are other ways to go about doing this.
One option, as you mentioned, is to create a nested selection with a separate <path> element for each point, and draw each path using a d3.svg.symbol() function. You can then customize the symbol function to be based on data or index.
var trips = d3.select("svg").selectAll("g.trips")
.data(/*The data you were currently using for each path,
now gets to a group of paths */)
.attr("class", "trips");
//also set any other properties for the each trip as a whole
var pointSymbol = d3.svg.symbol().type(function(d,i){
if (i === 0)
//this is the first point within its groups
return "cross";
if ( this === this.parentNode.querySelector("path:last-of-type") )
//this is the last point within its group
return "square";
//else:
return "circle";
});
var points = trips.selectAll("path")
.data(function(d) {
return get_activity_coords_(d.activities);
//return the array of point objects
})
.attr("transform", function(d){
/* calculate the position of the point using
your projection function directly */
})
.attr("d", pointSymbol);
Another option, which allows you to set custom shapes for the first and last point (but all intermediary points would be the same) is to connect the points as the vertices of a single, invisible <path> element and use line markers to draw the point symbols.
Your approach would be:
Create a <defs> element within your SVG (either hard-coded or dynamically with d3), and define the start, middle and end marker points within them. (You can use d3.svg.symbol() functions to draw the paths, or make your own, or use images, it's up to you.)
Use a d3.svg.line() function to create the path's "d" attribute based on your array of point coordinates; the x and y accessor functions for the line should use the projection function that you're using for the map to get the x/y position from the coordinates of that point. To avoid calculating the projection twice, you can save the projected coordinates in the data object:
var multipointLine = d3.svg.line()
.x(function(d,i) {
d.projectedCoords = projection(d);
return d.projectedCoords[0];
})
.y(function(d){ return d.projectedCoords[1];});
(You can't use your d3.geo.path() function to draw the lines as a map feature, because it will break the line into curves to match the curves of longitude and latitude lines in your map projection; to get the line markers to work, the path needs to be just a simple straight-line connection between points.)
Set the style on that path to be no stroke and no fill, so the line itself doesn't show up, but then set the marker-start, marker-mid and marker-end properties on the line to reference the id values of the correct marker element.
To get you started, here's an example using d3 to dynamically-generate line markers:
Is it possible to use d3.svg.symbol along with svg.marker
In a previous post called "D3: How to create slow transition of Circles for nodes in Force Directed Graphs FDG?", I got a great answer for how to transition a single element (e.g. the radius for "just circles") in D3.
My followup question is now about how to transition "multiple D3 attributes" at the same time...
As a reminder, I'm using D3 generated Radio Buttons to toggle the size of Nodes in a FDG Layout (on mouse click) from a default size to a scaled magnitude. You can find the Radio Buttons in the upper left hand of the Node Cluster Diagram (http://nounz.if4it.com/Nouns/Applications/A__Application_1.NodeCluster.html)
The code that toggles the node circles between a default number and a scaled magnitude (now using transitions) looks as follows...
var densityControlClick = function() {
var thisObject = d3.select(this);
var typeValue = thisObject.attr("density_type");
var oppositeTypeValue = (function() {
if(typeValue=="On") {
return "Off";
} else {
return "On";
}
})();
var densityBulletSelector = "." + "densityControlBullet-" + typeValue;
var selectedBullet = d3.selectAll(densityBulletSelector);
selectedBullet.style("fill", "Black")
var oppositeDensityBulletSelector = "." + "densityControlBullet-" + oppositeTypeValue;
var selectedOppositeBullet = d3.selectAll(oppositeDensityBulletSelector);
selectedOppositeBullet.style("fill", "White")
if(typeValue=="On") {
var selectedNodeCircles = d3.selectAll("#NODE");
selectedNodeCircles.transition().duration(500).attr("r", function(d){ return rRange(d.rSize); });
}
else {
var selectedNodeCircles = d3.selectAll("#NODE"); selectedNodeCircles.transition().duration(500).attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } );
}
}
Everything works great and you can see the slower node transitions when you select the radio buttons. However, I'd now like to learn how to transition multiple elements, such as the the radius and the edge lengths simultaneously, along with the theory behind doing so, in order to show off D3's dynamic nature.
My question is: Given that I already can successfully transition the radius of circles, how would I also transition other elements like the edge lengths based on attributes like "alpha", "friction", etc., and... what's the theory behind transitioning multiple elements (in other words, what does the code mean, in English)? The D3 API doesn't appear to clearly get into the theory behind transitioning multiple attributes, simultaneously.
So transitioning multiple attributes is the simple part of this question. Just like a regular selection you can set multiple attributes at a time on your transition:
selectedNodeCircles.transition().duration(500)
.attr("r", function(d){ return rRange(d.rSize); })
.attr("stroke", 'red');
This will transition your radius and your line colour. The transition is a property of the DOM element (in this case the circle) and it will transition as many DOM attributes as you like. The thing to remember is that there is only only one transition object on each DOM element. So if you create another you will overwrite the old one.
// This will NOT work
circles.transition().duration(1000).attr('r', 50);
// The radius transition will be overridden by the fill
// transition and so will not complete
circles.transition().duration(1000).attr('fill', 'red');
This can actually be quite useful because you don't have to worry about interrupting animations that are in progress and figure out how far along they are and then starting a new animation - this will generally be handled automatically.
In your case you want to transition edge lengths in your graph. These are determined by the positional attributes of the nodes. Judging by your finished product, these attributes are already being animated because you are updating the DOM on every iteration of the layout algorithm (not through transitions) probably in the tick() callback.
So you could use transitions inside your tick callback, which might look odd and may be a hassle to keep in synch with the radius transitions (you will have to set both attributes in the transition). But it might be just what you need.
Alternatively, if you can wait, don't update the DOM in the tick callback. Let the layout complete - it runs a lot faster when it is not rendering on each tick - and once it is complete you can animate the radius and x and y attributes to their final positions. Of course this means you'll want good starting positions.
I've been grappling with issues relating to transitions in D3. Consider this code:
svg.selectAll("path")
.data(data, key)
.enter().append("path")
.attr("d", someFunctionThatReturnsAPath);
});
And I call the following in a setTimeout a few seconds later:
svg.selectAll("path")
.transition()
.duration(2000)
.attr("d", someFunctionThatReturnsADifferentPath);
});
The second call correctly updates the paths but doesn't animate the transition. Why is there no transition when the d attribute is updated in the second call?
Note that the paths are very complex. In both calls, there's a noticeable delay before the paths are actually drawn. Perhaps that's related to the lack of transition?
I'm new to D3, but I've read up on transitions and can't seem to understand why this doesn't behave as I expect it.
Update
Per #Marjancek's answer, I'm providing more details regarding the two called functions.
Here is the definition of someFunctionThatReturnsAPath:
function(d) {
var coordinates = [];
for (var i = d.track.length - 1; i >= 0; i--) {
// We only care about the last 10 elements
if (coordinates.length >= 10)
break;
coordinates.push(d.track[i]);
}
return path({type: "LineString", coordinates: coordinates});
};
And someFunctionThatReturnsADifferentPath:
function(d) {
var coordinates = [];
for (var i = d.track.length - 1; i >= 0; i--) {
// We only care about the last 20 elements
if (coordinates.length >= 20)
break;
coordinates.push(d.track[i]);
}
return path({type: "LineString", coordinates: coordinates});
};
where path is defined as follows (projection is d3.geo.albersUsa()):
var path = d3.geo.path()
.projection(projection);
The objective is that on the second call, the line is extended with 10 newer data points.
If your paths do not have the same number of points, the transitions might not work as expected. Try .attrTween: http://github.com/mbostock/d3/wiki/Transitions#wiki-attrTween There is an example on bl.ocks.org but the site seems to be down at the moment so I can't link to it.
Added on edit: The gist I was thinking of was: https://gist.github.com/mbostock/3916621 the bl.ocks link will be http://bl.ocks.org/mbostock/3916621 when the site is back up.
It is impossible to know without looking at your someFunctionThatReturnsADifferentPath; but I'm guessing that your Different function does not take into account interpolation, from the three parameters it received.
Read the transitions documentation: https://github.com/mbostock/d3/wiki/Transitions