D3 - reordering elements based on their data - d3.js

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

Related

Adjusting small multiples sparklines

I have an heatmap that show some data and a sparkline for each line of the heatmap.
If the user click on a row label, then the data are ordered in decreasing order, so each rect is placed in the right position.
Viceversa, if the user click on a column label.
Each react is placed in the right way but I'm not able to place the sparkline.
Here the code.
When the user click on a row label, also the path inside the svg containing the sparkline should be updated.
And then, when the user click on a column label, the svg containing the sparkline should be placed in the correct line.
To place the svg in the right place, I try to use the x and y attributes of svg. They are updated but the svg doesn't change its position. Why?
Here is a piece of code related to that:
var t = svg.transition().duration(1000);
var values = [];
var sorted;
sorted = d3.range(numRegions).sort(function(a, b) {
if(sortOrder) {
return values[b] - values[a];
}
else {
return values[a] - values[b];
}
});
t.selectAll('.rowLabel')
.attr('y', function(d, k) {
return sorted.indexOf(k) * cellSize;
});
Also, I don't know how to change the path of every sparkline svg. I could take the data and order them manually, but this is only good for the row on which the user has clicked and not for all the others.
How can I do?
The vertical and horizontal re-positioning/redrawing of those sparklines require different approaches:
Vertical adjustment
For this solution I'm using selection.sort, which:
Returns a new selection that contains a copy of each group in this selection sorted according to the compare function. After sorting, re-inserts elements to match the resulting order.
So, first, we set our selection:
var sortedSVG = d3.selectAll(".data-svg")
Then, since selection.sort deals with data, we bind the datum, which is the index of the SVG regarding your sorted array:
.datum(function(d){
return sorted.indexOf(+this.dataset.r)
})
Finally, we compare them in ascending order:
.sort(function(a,b){
return d3.ascending(a,b)
});
Have in mind that the change is immediate, not a slow and nice transition. This is because the elements are re-positioned in the DOM, and the new structure is painted immediately. For having a slow transition, you'll have to deal with HTML and CSS inside the container div (which may be worth a new specific question).
Horizontal adjustment
The issue here is getting all the relevant data from the selection:
var sel = d3.selectAll('rect[data-r=\'' + k + '\']')
.each(function() {
arr.push({value:+d3.select(this).attr('data-value'),
pos: +d3.select(this).attr('data-c')});
});
And sorting it according to data-c. After that, we map the result to a simple array:
var result = arr.sort(function(a,b){
return sorted.indexOf(a.pos) - sorted.indexOf(b.pos)
}).map(function(d){
return d.value
});
Conclusion
Here is the updated Plunker: http://next.plnkr.co/edit/85fIXWxmX0l42cHx or http://plnkr.co/edit/85fIXWxmX0l42cHx
PS: You'll need to re-position the circles as well.

D3 creating a selector from this inside each

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.)

Is it possible to create pie charts with object consistency?

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/

select only updating elements with d3.js

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');
}

How to properly add and use D3 Events?

I'm having trouble understanding using D3 events and dispatch functions. I have a chart example that I've been working on called: "Vertical Bar Charts With Legends."
Drawing the charts and the legends was easy enough but I'd like to add the ability to highlight each bar as I mouseover its correlating text legend, located to the right of the chart.
I've read through all of the event documentation and even looked at a number of examples, most of which are pretty complicated, but I seem to be missing something. Would anyone know how to best accomplish the text legend mouseover functionality that dispatches events to automatically change colors of the corresponding vertical bars?
This question is similar to the one you posted in the d3-js Google Group. Without duplicating what I wrote there, I would reiterate that you probably don't want d3.dispatch; that is intended for custom event abstractions (such as brushes and behaviors). It'll be simpler to use native events.
If you want your legend to change the color of the corresponding bar on mouseover, then breakdown the problem into steps:
Detect mouseover on the legend.
Select the corresponding bar.
Change the bar's fill color.
First, use selection.on to listen for "mouseover" events on the legend elements. Your listener function will be called when the mouse goes over a legend element, and will be called with two arguments: the data (d) and the index (i). You can use this information to select the corresponding bar via d3.select. Lastly, use selection.style to change the "fill" style with the new color.
If you're not sure how to select the corresponding bar on legend mouseover, there are typically several options. The most straightforward is to select by index, assuming that the number of legend elements and number of rect elements are the same, and they are in the same order. In that case, if a local variable rect contains the rect elements, you could say:
function mouseover(d, i) {
d3.select(rect[0][i]).style("fill", "red");
}
If you don't want to rely on index, another option is to scan for the matching bar based on identical data. This uses selection.filter:
function mouseover(d, i) {
rect.filter(function(p) { return d === p; }).style("fill", "red");
}
Yet another option is to give each rect a unique ID, and then select by id. For example, on initialization, you could say:
rect.attr("id", function(d, i) { return "rect-" + i; });
Then, you could select the rect by id on mouseover:
function mouseover(d, i) {
d3.select("#rect-" + i).style("fill", "red");
}
The above example is contrived since I used the index to generate the id attribute (in which case, it's simpler and faster to use the first technique of selecting by index). A more realistic example would be if your data had a name property; you could then use d.name to generate the id attribute, and likewise select by id. You could also select by other attributes or class, if you don't want to generate a unique id.
Mike's answer is great.
I used it come up with this for selecting a cell in a grid I was drawing:
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y) #displays the cell x y location
d3.select(this).style("fill", "red");
So when I am entering the data in I added the event listener and using d3.select(this).
See the code in context below:
vis.selectAll("rect")
.data(singleArray)
.enter().append("svg:rect")
.attr("stroke", "none")
.attr("fill", (d) ->
if d.lifeForm
return "green"
else
return "white")
.attr("x", (d) -> xs(d.x))
.attr("y", (d) -> ys(d.y))
.attr("width", cellWidth)
.attr("height", cellHeight)
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y)
d3.select(this).style("fill", "red");
return
)

Resources