With a d3.js join Is there a way to select only the 'updating' elements separately from the 'entering' elements?
updateAndEnter = d3.selectAll('element').data(data);
entering = updateAndEnter.enter();
exiting = updateAndEnter.exit();
updatingOnly = ??;
Yes, the selection just after the data join contains the 'update only' elements. After appending to the enter() selection, it will be expanded to include the entering elements as well.
See General Update Pattern:
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().remove();
it's my pleasure
For me ( too ) this is a little bit confusing : it seems that the only available set is actually ENTER+UPDATE ( blended together ) and EXIT.
But what if i want to work or at least identify only updated elements?
I wrote a very simple function ( that follows, simply put wrap it in a script tag at the end of a basic html page ) showing this simple dilemma : how do I highlight updated elements ? Only ENTER and EXIT seem to react "correctly"
To test it, just type in chrome console :
manage_p(['append','a few','paragraph'])
manage_p(['append','a few','new','paragraph'])
manage_p(['append','paragraphs'])
I can get green or red highlighting, i can't get white
Maybe we're missing D3Js specs?
Best regards,
Fabrizio
function join_p(dataSet) {
var el = d3.select('body');
var join = el
.selectAll('p')
.data(dataSet);
join.enter().append('p').style('background-color','white');
join.style('background-color','green').text(function(d) { return d; });
join.exit().text('eliminato').style('background-color','red');
}
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
Everyone knows the update/enter/exit pattern:
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().transition().duration(2000).attr('transform',
rotation = 25;
return 'translate(' + d.x + ',' + d.y + ') rotate('+ rotation +' 30 30)';
}).remove();
}
My question is:
If I run this function while the exit is still occurring (during the 2000 ms transition window), how can I prevent the transition being called on elements already undergoing the exit transition.
In other words, when the original selection is made, it includes elements already in the exit().transition() state. So the exit transition is called on these elements a second time if they are not finished with their transition and removed().
Would I need to make a filter? Is there a test for in transition while exiting?
D3.js: Stop transitions being interrupted?
says give the exiting element a special class and then use that to only admit freshly exiting elements to the exit transition
.exit()
.filter(function(d,i) {
return !d3.select(this).classed("exiting");
})
.classed("exiting", true)
.transition()
...
A bigger problem will be any data that's re-introduced on your second click while it's still subject to an exit transition from the first. This in your example would disappear elements you want to keep as the exit transition finishes and calls a remove on that element. To get round this in the past I've added immediate (zero delay, zero duration) transitions to my update and enter selections to override/write the exit selection, if they don't have their own transitions already (if there's a better way someone let me know).
This leads to another point: set .classed("exiting", false) in the enter and update, so in the case of 'exiting'>'re-introduced'>'exiting again' elements the .exiting class isn't set and the exiting transition is activated.
I do not manage to update a bar-chart with nested data in D3.js with new data.
I have nested data of the form:
data = [[1,2,3,4,5,6],[6,5,4,3,2,1]];
I managed to visualize the data by first appending a group for every subarray.
In the groups I then add the arrays as data (simplified):
function createGraph(l, svg){
var g = svg.selectAll("g")
.data(l)
.enter().append("g");
var rect = g.selectAll("rect)
.data(function(d){return d;})
.enter().append("rect")
. ...
}
However, when call the function again with different data, nothing happens.
It seems like in the second row, the rects do not get updated.
I have created a full example over at jsBin: http://jsbin.com/UfeCaGe/1/edit?js,output
A little more explanation of Lars' bug-catch, since I'd already started playing around...
The key was in this section of the code:
var group = svg.selectAll("g")
.data(l)
.enter().append("g");
The variable group is assigned the enter selection, not the raw selection. Then in the next line:
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
You end up defining bar as only the rectangles that are children of just-entered groups. So even though you were handling update correctly for the rectangles, that whole section of code wasn't even running. You need to save the group selection before branching the chain to deal with entering groups:
var group = chart.selectAll("g")
.data(dt);
group.enter().append("g");
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
Also, you're missing a j in your function declaration in your update. And you can reduce code duplication by putting your rectangle update code after your rectangle enter code, and then any attributes that get set in the update don't have to be specified for enter. (Some older examples don't use this pattern, because the original versions of d3 didn't automatically transfer newly-entered elements to the main selection.)
// enter
bar.enter().append("rect")
.attr("fill", function(d,i,j){
return colors(j);})
.attr("height", 0);
// update
bar.attr("transform", function(d, i, j) {
x = "translate("+(i*2.2*w+j*w)+",0)";
return x; })
.transition()
.duration(750)
.attr("width", w)
.attr("height", function(d){return d*10;});
I would like to be able to place each data object (in this case 'moreData' array) inside a group element. So in the very simplified example below I would end up with three groups with 2 or 3 circles inside.
I'm using the node D3 provides with 'this' in a call to each (second one) to construct a selector.
Although the first call to each is correct (console.log tells me so)... the selector I create is obviously not doing the right thing as its creating 5 circles outside the body element and the second console.log never reports the first element.
Here is a fiddle simple use of this
From this simple data set of three objects:
data = [{'data':10, 'moreData':[1,2]}, {'data': 12, 'moreData':[3,4,5]},{'data':6, 'moreData':[7,8,9]}];
I expect and get three groups but no circles inside the groups.
var svg = d3.select("body").append("svg");
var shapes = svg.selectAll("g")
.data(data).enter();
shapes.append("g").each(add);
function add(d, i) {
console.log(i, d);
// this is where we go south!!
d3.select(this).data(d.moreData).enter() // help with this!!
.append("circle")
.attr("cx", function (d, i) {
return (i + 1) * 25;
})
.attr("cy", 10)
.attr("r", 10)
.each(function (d, i) {
console.log(i, d); // this is not good!
})
thanks for any insight into what I'm doing wrong....
The above fiddle shows no output, but if you inspect the 'results' tab you can see the correct empty groups and the circle elements outside the body tag ... at least in Chrome.
You need to select the empty set of circles before setting the data.
Right now, you are calling:
d3.select(this).data(d.moreData)
Replace that line with:
d3.select(this).selectAll("circle").data(d.moreData)
The general d3 enter paradigm is select a group -> attach data to that group -> use enter/exit, where enter will run for each item in the group which has data, but no DOM element, and exit for each element which has a DOM element but no data.
Fiddle.
Also, you should use different variables for i and d for your inner function, since right now they're the same as the variables on your outer function. (Perhaps use function(D, I) instead.)
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/