I was wondering how would it be possible to associate a certain key function with a datum data binding in D3. This seems to be possible with .data([values[, key]]), but the key is not available as parameter in the .datum([value]) binding.
This becomes particularly relevant when drawing SVG paths in which the updated values are not appended to the end of the data array, but contribute to changes in granularity in the middle.
This example illustrates this case:
http://jsfiddle.net/vastur/LtHyZ/1/
Each data point is an [(x),(y)] tuple. The red dots are moving properly according to the key function on the x axis:
.data(lineData, function(d) {return d[0]})
But the line is created using datum() and therefore no key function is associated. Consequently, its line segments move illogically when new data points are added in between.
So, in this example, how to make line vertices move according to the motion of the red dots?
The short answer is that this is not possible for the case that you're dealing with. The problem is that there is only a single element (the line), so matching and computing joins doesn't make sense. To achieve the behaviour you want, you would need to specify the same number of support points for both lines.
A slightly more elaborate explanation. The D3 data model relies on binding data points to DOM elements. That is, each data point is matched to exactly one DOM element. The path you're drawing is a single DOM element and therefore has only one data "point" matched to it, the point being an array in this case. You can't use D3's data model for what you want precisely of this -- there's only a single DOM element for the path. Furthermore, SVG paths are entities that cannot be broken easily -- you need an M command to start with and then L commands in your case. You can sort of do it by breaking the line into different segments, but then the animation doesn't work (see here).
The way to achieve what you want would be to preprocess the data to compute any intermediate points.
Related
I'd like to transition between curve types using D3.js.
Take a look at this block. The data stay the same but the curve type changes. I was expecting the paths to maintain their approximate positions on the plane -- the data stay the same, after all -- but they don't. The paths appear to be redrawn, although I don't understand why with basis to linear the paths seem to be redrawn from left to right whilst with linear to basis the paths seem to be redrawn from right to left.
I've read Mike Bostock's post on Path Transitions, but I think this is a slightly different problem. There, the data change but the curve type remains the same. Here, the data stay the same but the curve type changes.
Thanks in advance for any help!
To understand why you have such a strange transition, let's compare the d attribute of the paths using a curveBasis and a curveLinear.
First, a curveBasis:
d="M0,101.2061594964L45.48756294826797,89.52282837400001C90.97512589653594,77.83949725160001,181.95025179307189,54.47283500680002,271.46268884480395,84.08731623460001C360.975125896536,113.70179746240001,449.0248741034641,196.2974221628,538.5373111551961,222.09899531679994C628.0497482069281,247.90056847079998,719.0248741034642,216.90809007840002,764.512437051732,201.4118508822L810,185.915611686"
Now a curveLinear (same data):
d="M0,101.2061594964L272.92537768960784,31.10617276200003L537.0746223103922,278.89304686319997L810,185.915611686"
As you can see, the path is way simpler with curveLinear. So, the strange transition is the expected behaviour.
A possible solution is using a path interpolation, as proposed in this code from Mike Bostock.
Here is your bl.ocks with a path interpolation: http://blockbuilder.org/anonymous/02125b1fb145a979e53f369c4976a772
PS: If you want to avoid that strange transition when you load the page (all paths coming from the top left corner), draw them the first time using a regular attr method.
I'm working on a motion chart showing the development of certain data points through time by updating the position and size of the corresponding circle on a chart (similar to https://bost.ocks.org/mike/nations/ ).
Since circles can overlap I need the smallest one to be drawn on top.
In the example this is accomplished by calling selection.sort(), but if I do the same (with the newer D3v4) I get an unexpected behaviour. The sort seems to switch the data objects associated to the visual circle object.
Check this fiddle to see. Run it once as it is (without calling sort(order) in line 45) - this is the expected behaviour. The y value of each object doesn't change so the circles should move on a horizontal line. Now uncomment line 45 to call the sort on each update and run it again. This time the paths of the circles suddenly cross (because the underlying object is switched).
https://jsfiddle.net/orj1rcy8/1/
The API states selection.sort() Returns a new selection that contains a copy .... I assume this is the problem, however I don't understand at this point how the correct approach would look like.
Short answer
You need a key function when you bind your data:
.data(dataForKey(keyIndex), function(d){ return d.name})
Here is your updated fiddle: https://jsfiddle.net/7kyvzkwe/
Long answer
What happened here is that you were victim of what we call object constancy, or, more precisely, not properly setting an object constancy (here is a good reading about it, written by Mike Bostock).
The problem is that, in D3, the data is bound to the elements in their order:
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on.
So, let's see what happened. Your code has three circles, in this order:
The smallest circle, r = 3;
The medium circle, r = 10;
The largest circle, r = 15.
The data is appended in that above-mentioned order. But then, when you do sort(order):
function order(a, b){
return radius(b) - radius(a);
}
You sort the elements, and now you have:
The largest circle, r = 15;
The medium circle, r = 10;
The smaller circle, r = 3.
And then comes the problem: next time you run the next function and bind the new data, you're binding the data to DOM elements in that new order. That is, the data regarding the smallest circle is being bound to the largest circle in the DOM. Using the names of your elements, you're binding the data regarding item1 to the item3 in the DOM (given you have 3 elements, the only one always receiving the correct data is item2).
You can understand this better in the next fiddle. In this next fiddle, I'm using exactly your code, uncommenting the sort function. But, unlike your original code, here I'm changing the order of the data, so we have the largest circle first, then the medium, then the smallest:
[{name:"item1", x:1, y:2, z:15},
{name:"item2", x:1, y:4, z:10},
{name:"item3", x:1, y:6, z:3}];
You can see that, even calling sort, the circles stay in their positions. Here is the fiddle: https://jsfiddle.net/9p1tL43j/
That happens because the next time the function next runs, the data for the smallest circle is bound to the smallest circle (in the DOM), and so on...
Thus, in a nutshell, you need a key function if you want to keep the object constancy. A key function:
... may be specified to control which datum is assigned to which element, replacing the default join-by-index.
I want to be able to implement the D3 General Update Pattern III as a D3 pack layout. This means that each char would be inside a circle and that the char circles would be inside a larger enclosing circle. Everything should stay consistent with the GUPIII except that, of course, the positioning of the chars will be in a larger circle instead of on a horizontal axis.
I have tried to follow the same GUPIII structure including the important aspect of using a data join with key.
var alphaBubble = d3.layout.pack()
.size([diameter, diameter])
.padding(5);
var nodes = alphaBubble.nodes(data);
var vis = svg.selectAll("circle")
.data(nodes, function(d) { return d; });
But data join with key does not seem to work with the pack layout.
I'm having trouble getting my head around possible alternative mechanisms such as pack.value or nest. I have not been able to grasp whether they would somehow accomplish what is going on in GUPIII where incoming (enter) chars are green, existing (update) chars are black, exiting chars are removed.
Join by key is important in order to maintain the relative position of the char. I realize that this aspect of relative position is not as easy to see in the pack layout as it is in the linear axis layout, but I have a necessity for this which is not apparent in this example. I just need to successfully implement a "by key" join with pack that retains existing nodes when new data comes in.
Typically what I see with the pack layout is that either no nodes are replaced on data updates (maybe only attributes such as size are altered), or else all nodes are replaced with new incoming data, depending on the context. But I want to retain the nodes that are the same (by some key, in this case the char itself), remove the ones that are no longer present, and add the new ones "by key". Can anyone help me translate the D3 General Update Pattern II to a pack layout? I would include my full code attempt, but I really think just looking at the GUPIII is more clear if you are familiar with how the pack layout can facilitate join by key on update.
Thank you.
Ok, through much trial and error and study of existing examples I have been able to solve my question as to how to implement the D3 General Update Pattern III as a pack layout. I was not able to find any example that used data join by key, which is necessary for object constancy on update, so I'm hoping my example may provide some guidance for others who need the same pattern of behavior.
My solution is here. And my Javascript code is here.
I will point out a few important highlights. First, unlike in the linear non-pack version, the data join-by-key value must be accessed as an object property (i.e. d.id) instead of directly (i.e. d).
// Data join by key to <g> nodes
var node = svg.selectAll(".node")
.data(nodes, function(d) {
return d.id;
});
// Data join by key to circles
var circles = svg.selectAll("circle")
.data(nodes, function(d) {
return d.id;
});
Next, since the alphabet character is represented in a circle, both the circle and the char must be contained in a <g> element, i.e. node. And here was the trickiest part: the data needs to be joined by key separately to both the node and the circle elements. This is because the pack layout has performed the necessary calculation of scale (i.e. circle radii) and coordinate positions. So the circles need the data's calculated radii and the <g> node elements need the calculated coordinate positions. So a separate data join was required for each.
Once I figured this out, all of the necessary operations and references fell into place and the pack version of the General Update Pattern started working properly. Be sure to compare the linear and pack versions to each other.
The alphabetization sort is, I guess, unnecessary since the pack layout makes no accommodation for ordering. But I retained the sorting for consistency and because I wanted to observe its effect.
I got a data set that where each sample has a size (0-1000) and a value (grade 1-5). I want to visualise the data with circles of different sizes along a line (domain axis), much like:
http://www.nytimes.com/interactive/2013/05/25/sunday-review/corporate-taxes.html?_r=1&
(note that circles even with the same effective taxrate do not overlap)
Example data:
sample 1: size 300 value 3.2
sample 2: size 45 value 3.8
sample 3: size 4400 value 4.0
sample 5: size 233 value 0.2
sample 6: size 4000 value 4.2
How can the data above be visualised using circles on a line (size decides diameter, value decides approximate position on the line) so that circles do not overlap?
I've been looking at D3's packing layout, but from what I can tell it doesn't support this out of the box. Anyone got any ideas on how to approach this?
Oooh, this one was a puzzle...
If you look at the code for the NYTimes graphic, it uses pre-computed coordinates in the data file, so that's not much use.
However, there's an unused variable declaration at the top of the script that hints that the original version used d3.geom.quadtree to lay out the circles. The quadtree isn't actually a layout method; it is used to create a search tree of adjacent nodes, so that when you need to find a node in a given area you don't have to search through the whole set. Example here.
The quadtree can therefore be used to identify which of your datapoints might be overlapping each other on the x-axis. Then you have to figure out how much you need to offset them in order to avoid that overlap. The variable radii complicate both functions...
I've got a test case implemented here:
http://fiddle.jshell.net/6cW9u/5/
The packing algorithm isn't perfect: I always add new circles to the outside of existing circles, without testing whether they could possibly fit closer in, so sometimes you get significant extra whitespace when it is just the far edges of circles bumping into each other. (Run it a few times to get an idea of the possibilities -- note that I've got x-variables distributed as random normal and r-variables distributed as random uniform.) I also got a stack overflow on the recursive methods during one iteration with N=100 -- the random distribution clearly wasn't distributed well enough for the quadtree optimization.
But it's got the basic functionality. Leave a comment here if you can't follow the logic of my code comments.
--ABR
Update
New fiddle here: http://fiddle.jshell.net/6cW9u/8/
After a lot of re-arranging, I got the packing algorithm to search for gaps between existing bubbles. I've got the sort order switched (so that biggest circles get added first) to show off how little circles can get added in the gaps -- although as I mention in the code comments, this reduces the efficiency of the quadtree search.
Also added various decoration and transition so you can clearly see how the circles are being positioned, and set the r-scale to be square root, so the area (not radius) is proportional to the value in the data (which is more realistic, and what the O.P. asked for).
D3's packing layout is not the answer here. It places circles in a spiral fashion around the existing group. Here's me reverse-engineering the algorithm behind packing layout:
I would suggest a force layout-based approach. That way, you can give your nodes force towards a gravitational center, and then let gravity do its thing.
Force layouts (e.g. Clustered Force Layout I) are usually animations, so you'll want to apply a static force layout.
I've wrapped up this approach in an example block, which looks like this:
I use d3.js exactly as shown here but with different data value.
When the graph is first shown all elements are scattered and for around a second rapidly move towards their position. This looks nice in the sample, but for my data it does not look so nice. Any way to turn it off so that nodes start in their designated place? Any way to customize this entry visualization?
You can loop through the nodes array and set the .x/.y and .px/.py values to an initial position that you want the nodes in, and by doing so will limit the amount they need to move in their initial layout. If you turn off the force-directed algorithm (the tick() function) then you can apply standard .transtion().duration(500).attr("transform", xx) behavior to the nodes and links to make them animate in a manner you prefer before or after running the force-directed algorithm, but make sure you set the .x/.y and .px/.py attributes to their final position, otherwise they will be reset as soon as you .tick()