Confused by selection.filter - d3.js

I'm having a hard time understanding selection.filter. I tried this, in a blank document:
d3.selectAll('rect')
.data([1, 2, 3])
.filter(function(d) { return d>1; })
// [ Array[0] ]
I expected the selection to have 2 elements, but it has 0. Maybe it's because I'm working with an empty update selection...
d3.selectAll('rect')
.data([1, 2, 3])
.enter()
.filter(function(d) { return d>1; })
// []
Now it seems I don't even have a selection.
If the DOM elements exist
d3.selectAll('rect').data([1, 2, 3])
.enter().append('rect')
and then I select and filter
d3.selectAll('rect').filter(function(d) { return d>1; })
// [ Array[2] ]
it seems to work. So what's going on here? Filter seems to work in this example on selections that don't correspond to any DOM elements, so I'd expect it to work in my first example above.

The .filter() function only works on selections that have data bound to them already, i.e. not after you've called .data() on them. The example you've linked to doesn't actually work the way it seems to do -- let me reindent for clarity:
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; })
)
.enter().append("g")
The .filter() function isn't actually applied to a selection here, but to the array that bubble.nodes() returns. This is then passed to .data(). And this is exactly what you would do in a case like yours -- there's no need to filter the selection if you can filter the data that determines it.

Related

Reorder elements on mouse over (to solve Z-index conflict) when data is bound through `selectAll.data(data)`

I am working on a d3 plot, where I have multiple elements which might overlap when drawn.
Each element renders a timeline and has multiple graphical units (start circle, line and end circle), something like as below:
O----------O O
O--------------------O
O-------O-----O-------O
For example the third line has two timeline plot elements which are overlapping as start time of the 2nd timeline is before end time of the first timeline. Note that 2nd timeline in the first line has only start time (as end time and start time are same).
Now, the following code brings an element of the timeline to front on mouseover by moving the DOM node to be the last child of its parent.
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
But the problem is that this is not altering the order of the bound data and is breaking the overall plot.
Each of the plot element has specific order in the dom which is bound to the d3 data in the same order. When the code above changes the order to bring any element to the front it is breaking the order, it still thinks that the order of the children are the same, which is wrong.
Here is a sample JSFiddle to describe the issue:
https://jsfiddle.net/pixelord/g2gt1f03/57/
How can I retain the data order once I have altered the dom elements?
Thanks.
Instead of doing the html update by yourself let d3 do it, remember that d3 stands for data driven documents so rewrite your problem as
On mouseover move the selection's datum to the last position and then rerender the graph
Imagine that your data is [0,1,2,3], when you mouseover on any element that represents the second datum you move the second datum to the last position i.e. [0,2,3,1] and that's pretty much it
.on("mouseover", function() {
var selection = d3.select(this);
var d = selection.datum()
// find d in data, extract it and push it
var index = data.indexOf(d)
var extract = data.splice(index, 1)
data = data.concat(extract)
draw()
});
Next when you bind your data make sure you add a way to distinguish from both states which is done with the second parameter sent to the .data() function which might be an id
var data = [
[5, 8, 6],
[10, 10, 6],
[20, 25, 6],
[23, 27, 6]
].map(function (d, i) {
return {
id: i,
x1: d[0],
y1: d[2],
x2: d[1],
y2: d[2]
}
});
// ...
var itemGroup = maingroup.selectAll(".itemGroup")
.data(data, function (d) { return d.id })
Finally you'll need to tell d3 that we have modified the order of the elements and that it needs to do what you were doing by hand which is reorder the elements
// update
itemGroup.order()
Demo
I like the way Mauricio solved the issue.
However, after some investigation I came to know that I can specify key value while binding the data. So here is my solution without re-ordering the data itself:
I added a key value property for each data object which is unique.
I specify the key value while binding the data like,
data(data_list, funcion(d){return d.keyValue})
the problem was fixed.

d3 selectAll: count results

How do I count how many nodes were matched by a selectAll? (without joined data)
Or if there's data, how to count the data from the selection? (suppose I've set it with "data(function...)" so I don't know the length in advance)
Just use d3.selectAll(data).size().Hope this example help you:
var matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
var tr = d3.select("body").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr");
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.text(function(d) { return d; });
var tdSize=tr.selectAll("td").size();
Complete jsfiddle here.
If you want the length conveniently from a callback function, such as setting an element attribute, it seems that you can get it from the third argument, like so:
node
.attr('some-property', function(datum, index, array) {
// d/datum = the individual data point
// index = the index of the data point (0, 1, 2, 3, etc)
// array = full array of data points
// array.length = the size/length of the data points / dataset
// some calculation involving array.length or whatever
return someValue;
});
Similar to the call signature of the JavaScript forEach/filter/etc. array functions.
Seems like most of the d3 functions support this:
https://github.com/d3/d3-selection
...current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i])
...is a repeated phrase throughout the docs. So if you see a d3 function where you'd use d, you can probably also get index and array.
One way I have done this previously is to pass that information into the data function by making a new object.
.data(function(d) {
return d.Objects.map(function(obj) {
return {
Object: obj,
TotalObjects: d.Objects.length
}
});
Then in your update portions you use Object and still have the count available.
.attr("x", function(d) {
return d.Object.X;
})
.attr("y", function(d) {
return d.TotalObjects;
})
To get the data count, then after .selectAll() and .data(), it appears that .enter() is needed before .size():
legend_count = legendBoxG.selectAll("legend.box")
.data(curNodesData, (d) -> d.id)
.enter()
.size()
Without the .enter(), the result is 0. The .enter() makes it return the data count. (Code above is shown in Coffee dialect.)
I need to get the count before adding attributes to my svg objects (in order to scale them properly), and none of the preceding examples did that. However I can't seem to add more attributes after stripping out the count into a variable as above. So while the above approach demonstrates the operation of data() and enter() it isn't really a practical solution. What I do instead is to get the length of the data array itself before doing the selectAll(). I can do that most simply with the length property (not a function) on the data array itself:
legend_count = curNodesData.length

Get one element from d3js selection, by index

I've created a set of d3js elements based on an array of 3 elements:
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter().append('circle');
edit:
How can I select the second element by index?
The most natural way to manipulate just one element is using the filter function:
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle');
var filteredCircleSet = circleSet
.filter(function (d, i) { return i === 1;})
// put all your operations on the second element, e.g.
.append('h1').text('foo');
Note that depending on what you do with the other elements you might use one of the two variants of this approach:
variant a): use the filter in the data function (to reduce the data and the appended elements)
variant b): use the filter to exclude instead of to include in order to remove the other elements at the end
See also Filter data in d3 to draw either circle or square
One other way to do it is to use the selection.each method: https://github.com/mbostock/d3/wiki/Selections#wiki-each
By using an if statement with the corresponding index you can create a block for one element.
E.g.
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
// put all your operations on the second element, e.g.
d3.select(this).append('h1').text(i);
}
});
In d3 v4 and above, you can use Selection.nodes(). Assuming i is the index number you want:
d3.select(someSelection.nodes()[i])
It's a natural one-liner, and it's arguably more readable: you're obviously just getting the node at i in the order, as a D3 selection.
It looks like it'd be more efficient than the alternatives, which involve looping through the entire selection with .each(). So, you might think this is O(1), while the other options are O(n).
Unfortunately, Selection.nodes() itself includes an each loop, so it's also O(n) (not that it's likely to matter in real life unless you call this thousands of times on selections with thousands of nodes):
var nodes = new Array(this.size()), i = -1;
this.each(function() { nodes[++i] = this; });
return nodes;
However, this way you can separate the looping from the getting, which could be useful if efficiency is a major concern.
For example, if you want to loop through each() in selection A and get the item in the same position from selection B, and you want to avoid loops-within-loops because those selections can be huge and you call this many times, you could structure it like this, which would be O(2n) instead of O(n^2):
var selectionBArray = selectionB.nodes()
selectionA.each(function(d, i) {
var iFromSelectionA = this
var iFromSelectionB = d3.select(selectionBArray[i])
})
...or if you're using arrow functions to preserve this context:
var selectionBArray = selectionB.nodes()
selectionA.each((d, i, nodes) => {
var iFromSelectionA = d3.select(nodes[i])
var iFromSelectionB = d3.select(selectionBArray[i])
})
You could even (ab)use Selection._groups, but I wouldn't recommend using a private property like that since it'll break if a D3 update renamed the _groups property, like this update did.
Use the preset function i variable, which references the index of the array object.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('fill',function(d,i){i === 1 ? return 'red' : return 'black' };
Find more on array structure references in d3.js at this tutorial
You can also encode each element you append by utilizing the count of the i index when assigning a class.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr("class",function(d,i){ return "yourclass item" + i })
var theSecondElement = d3.select(".item1")
Last, you could use the .each method and a conditional to target a specific element
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
var that = this;
(function textAdd() {
d3.select(that).append('h1').text(i);
)();
}
});

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

D3 - Nesting multiple charts from the same dataset

I have the following code;
var matrix = [
[ 1, 2, 3, 4],
[ 5, 6, 7, 8]
];
var tr = d3.selectAll("td.sparkline").append("table").selectAll("tr")
.data(matrix)
.enter()
.append("tr");
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter()
.append("td")
.text(function(d) { return d; });
What is the correct way to do the following;
<td class="sparkline">matrix[0]</td>
<td class="sparkline">matrix[1]</td>
Can I use the same class for the container?
I keep getting both arrays in both rows.
I read about the nesting here
http://bost.ocks.org/mike/nest/
but I still can't figure it out.
It's not clear what you're trying to accomplish here. Do you have a table with a td element with class "sparkline" already in the DOM, and you're trying to add a new table inside it? Or do you want a new table somewhere else? Do you want one cell per number in your matrix, or one cell per row in the matrix?
This fiddle illustrates the above choices: http://jsfiddle.net/nrabinowitz/ma7Dn/
In the first version, the code is as you've provided it, and there's an existing table in the DOM with a td.sparkline element to append new content to. The result is a new table within that td, with rows and cells corresponding to your matrix structure.
In the second version, we're appending a new table to a different element, with one row per row in the matrix and one cell per row:
var tr2 = d3.select("#v2").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr").append("td")
.attr("class", "sparkline")
.text(function(d) { return d.join(", "); });
The first version works as expected with your own code, so I'm assuming that's not what you want. My guess is that your initial selector td.sparkline is the cause of the confusion; if that's not referencing something already in the DOM, it's an error here.
Figured it out...
d3.selectAll('td.sparkline')
.data(matrix)
.text(function(d, i)
{ return 'Result #' + (i + 1) + ': ' + d; // i is 0-based.
});
gives me the ability to add each index to the correct row.
This link was pretty helpful;
http://code.hazzens.com/d3tut/lesson_1.html

Resources