How to guarantee old data is in d3's exit selection - javascript

Problem:
I'm trying to understand the behavior of d3's exit selection from the general update pattern.
Note: I'm using d3V5
Fiddle
Say I want to visualize the number "1".
var data = [{id:"1"}];
var text = svg.selectAll('.text').data(data);
text.enter()
.each((d) => console.log("first append " + d))
.append('text')
.text(d => d.id)
All well and good. But now say I'm tired of "1" and more interested in visualizing "2".
data = [{id:"2"}];
text = svg.selectAll('.text').data(data);
text.exit().each((d) => console.log("remove " + d)).remove();
The console does not log {id:"1"}. This item was not placed in the exit selection.
text.enter()
.each((d) => console.log("now append " + d))
.append('text')
.text(d => d.id)
Now I have a "1" and a "2" stacked right on top of one another.
Assumptions:
I had thought that when I do .data(data) d3 would do a diff between the dom and the data, and place any old dom nodes without corresponding entries in data in the exit selection. I had thought the 'id' field on the data would distinguish these data elements. That doesn't seem to be the case.
Question:
How do I get {id:"1"} in the exit selection?
Or how do I remove the dom node associated with {id:"1"}?

The confusion here began with an erroneous assumption. In most of the d3 examples I've seen, the data has the following format:
[ {'id': 1, 'info': 'something'}, {'id': 2, 'info': 'something else'}, ...]
I had been assuming that selection.data() performed a diff using the data's 'id' field by default.
It turns out that this isn't the case, and you need to offer your own key function.
From D3's selection docs:
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 I added a key function:
function idFunc(d) { return d ? d.id : this.id; }
var data = [{id:"1"}];
var text = svg.selectAll('text').data(data, idFunc);
text.enter()
.append('text')
.text(d => d.id)
Then, dom nodes no longer corresponding to items in the data array found their way into the exit selection, and I was able to remove them.
Conclusions:
(Always) Define a key function over your data when you .data(data, keyFunc)

Related

What is enter and exit in d3.js?

I tried to understand with this example but couldn't understand exit function. Actually before exit I got what enter is but when I implemented the exit function right after enter function not appended li items created even if I bound different data to the exit function.
Please help me to understand the missions of enter a
<ul id="#example">
<li></li>
<li></li>
<li></li>
</ul>
<script>
var updateSelection;
var veri;
window.onload = function() {
// Bağlanacak veri sayısı 5
veri = [5, 10, 15, 20, 25];
updateSelection = d3
.select("ul")
.selectAll("li")
.data(veri)
.enter()
.append("li")
.text(d => d)
.style("color","blue");
console.log(updateSelection);
updateSelection = d3
.select("ul")
.selectAll("li")
.data([1,2])
.exit()
.append("li")
.text(d => d)
.style("color","red");
console.log(updateSelection);
}
</script>
In d3JS v4 and 5, if you create a selection and apply the .data() function, that changes the nature of the selection object you have. If you store that in a variable, you can then subsequently call the .enter() and .exit() functions on that object to select the items that should be added and the items that should be deleted, respectively. You can then merge the new items with the pre-existing ones using the .merge() function, which, as the name applies, merges those into a single selection, kind of like the result of an entirely new d3.select() call would be.
With regards to your code, you should not have to make the selection multiple times, and I think calling .data() multiple times can do more harm than good. I'll add some code in which I draw a legend to a chart. I hope it helps illustrate my point.
// I select all legend values and apply a new set of data to it
let items = this.legend
.selectAll(".item")
.data(values);
// I remove the values that can be removed
items.exit().remove();
// I specifically select the new items and draw a new <g> element for each
const enterItems = items.enter()
.append("g")
.classed("item", true);
// Each of those <g> elements gets a <circle> and a <text> node
enterItems.datum(d => d)
.append("circle")
.attr("r", 3);
enterItems.datum(d => d)
.append("text")
.attr("dx", -7)
.attr("dy", 3);
// Finally I merge the new <g> elements with the ones that I left over
// from before I called `.data()` - so not the ones I deleted
// It's good practice not to apply any code to this merged selection
// that involves drawing new items, but to only respond to changes in data,
// i.e. calculating and setting dynamic variables.
items = enterItems
.merge(items)
// Apply to all because this redraw
// might have been caused by a resize event
.attr("transform", (_, i) =>
`translate(${this.$element[0].clientWidth - this.margin.right},
${this.margin.top + i * 10 + 5})`);
// This also applies to all child elements. I may change the color and the text,
// but I do not recalculate the fixed size of the circles or the positioning of the
// text - because I know those would never change
items.select("circle")
.style("fill", d => this.colors(d))
.attr("class", d => `legend item-${d}`);
items.select("text")
.text(d => d);
There are good tutorials talking about D3 selections, this quite old one being a good read: https://bost.ocks.org/mike/join/
Regarding your question, it's not clear why you're appending elements to an exit selection. However, it's worth mentioning that one can do whatever they want with the exit selection, including appending elements.
That being said, in short, this is what's happening in your code:
You are using this data...
[5, 10, 15, 20, 25]
... to the enter selection. As you can see, there are 5 elements in the data array, so the enter selection has five elements. Have a look at the console:
var veri = [5, 10, 15, 20, 25];
var updateSelection = d3
.select("ul")
.selectAll("li")
.data(veri)
.enter()
.append("li")
.text(d => d)
.style("color", "blue");
console.log("size of enter selection: " + updateSelection.size());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<ul></ul>
Then, you select everything again and bind the selection to this data array:
[1, 2]
As you can see, you had 5 elements in the previous data array and 2 elements in the new data array (because you don't have a key function here, you're binding the elements by index). So, since you have 3 data elements without a corresponding DOM element, your exit selection size is 3. Compare the two arrays:
[5, 10, 15, 20, 25]
[1, 2] | | |
| | |
V V V
//these elements belong to the exit selection
Have a look at the console again:
var veri = [5, 10, 15, 20, 25];
var updateSelection = d3
.select("ul")
.selectAll("li")
.data(veri)
.enter()
.append("li")
.text(d => d)
.style("color", "blue");
updateSelection = d3
.select("ul")
.selectAll("li")
.data([1, 2])
.exit()
.append("li")
.text(d => d)
.style("color", "red");
console.log("size of exit selection: " + updateSelection.size());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<ul></ul>
Finally, the visual we have is exactly the expected one: we have 5 blue elements, corresponding to the enter selection, and 3 red elements, corresponding to the exit selection (which, for whatever reason, you appended instead or removed).

d3.js. How to update nested nodes?

Here is a jsfiddle. I expect that after second step when i update table rows with new data set it will show 3 and 4. But it still show 1 and 2. Why? Why nested elements still keep old data set? How to fix it? How to update nested tags?
const data1 = [1, 2];
const table = d3.select('body')
.append('table')
.append('tbody');
table
.selectAll('tr')
.data(data1)
.enter()
.append('tr')
.append('td')
.text(function(d) {
return d;
});
const data2 = [3, 4];
table
.selectAll('tr')
.data(data2)
.selectAll('td')
.text(function(d) {
return d;
});
To answer your comment question, this is a subselection select. From the docs:
Unlike selection.selectAll, selection.select does not affect grouping: it preserves the existing group structure and indexes, and propagates data (if any) to selected children. Grouping plays an important role in the data join. See Nested Selections and How Selections Work for more on this topic.
The important part here is propagates data to selected children.

Data keyfunction filter always returning first element

Short version: DOM elements for first data element in bound d3.js JSON dataset is always showing and can't be removed, all other elements behaving as expected.
Working with d3.js, trying to generate a standard schedule setup that is subsequently populated with varied datasets. Pull some nested JSON data, standard enter/update/exit format to modify schedule display. Need the structure (struct) data available elsewhere, so assign it to a global after pulling before filtering and attaching:
d3.json('php/controllers/sched_struct_endpoint.php?set=' + set, function(d) {
struct = d;
drawAxis();
});
(drawAxis() is the redraw function, code below from that function). Everything is working fine, except that the first element (schedule display for Sunday) is always bound/showing - all other elements add/remove exactly as expected. Think the issue might be with my data filter; using day number as the key function, intending to display all days if 'All' is selected, or only the specified day if a single day is selected. Sunday is day 1, Monday 2, etc. - but the resulting data display is always [1,2] or [1,5] - never just [2] or [5]. Display all [1,2,3,4,5,6,7] works as expected.
dayData = chartAxis.selectAll(".titleDay")
.data(struct, function(d) { return day === 'All' ? d.day_number : d.day_number === day; } );
var titleDay = dayData.enter()
.append("g")
.attr("class","titleDay");
titleDay
.attr("id", function(d) { return d.day_name;})
.attr("transform", function(d,i) { return "translate(0," + (day == 'All' ? i * elem_height * 1.05 : 0) + ")" ; })
titleDay.append("rect")
.attr("width",daywidth)
.attr("height", elem_height);
titleDay.append("text")
.text(function(d) { return d.day_name; })
.attr("text-anchor","middle")
.attr("transform", function(d,i) { return "translate(45," + (elem_height / 2) + ")" ; });
... (more code adding additional schedule elements) ...
dayData.exit().remove();
Experimentally adding
d3.selectAll(".titleDay").remove();
to the beginning of the redraw function will temporarily remove the first element, but then it gets added back in immediately.
Sample data structure (array of 7 objects, each with day name, number, and list of available resources):
[
{
"day_number": "1",
"day_name": "Sunday",
"resources": [
{
"resource_id": "OR01",
"resource_name": "OR 01",
...
It looks like you're trying to use the key function to filter data -- this is not what it does. You probably want something like
.data(struct.filter(function(d) { return day === "All" || d.day_number === day; }),
function(d) { return d.day_number; })
This will filter the data according to the criteria that you specified and then do the matching by day_number.

Return D3 key of Selected

I know I can use d3.keys() to return all keys inside of an object, but I want to return the selected items key I'm targeting inside of a mouseover event.
I'm targeting elements in D3 like so:
var test = something.selectAll('rect')
.data(myData['groupSelection'])
.enter()
.append('rect')
.on('mouseover', function (d) {
console.log(d3.keys(d));
}
This will return that given selections keys though, when I really need a count of that items keys, for instance, if I select the second rect created from the data, it'd be nice for it to return 2.
All callbacks in D3 that get the data as an argument also get the index of the data as an argument. That is, instead of
.attr("foo", function(d) { ... });
you can also write
.attr("foo", function(d, i) { ... });
where d is the data and i the index of d in the array of data that you've passed to .data(). The same goes for .style(), .on(), etc.
For example, assume you have data [2,3] and elements with data 1 and 2 bound to them. Now if you do (note the key function to .data() to match elements by their contents)
var sel = d3.selectAll("element").data([2,3], function(d) { return d; });
you'll get non-empty enter (containing 3), update (containing 2) and exit (containing the element that 1 was bound to) selections. You can operate on each of these selections, e.g.
sel.attr("foo", function(d, i) { ... });
The i refers to the index within the selection. Each selection contains only one element, so you'll get 0 for i -- for each selection. That is, the code
sel.attr("foo", function(d, i) { console.log(i); });
sel.enter().attr("foo", function(d, i) { console.log(i); });
sel.exit().attr("foo", function(d, i) { console.log(i); });
will log 0 to the console three times. If your update selection was of length 3 for example (that is, three elements in the argument to .data() are matched up with DOM elements in the selection), you would get 0, 1, 2 on the console.

difference between function(d) and function(d,i)?

Every D3js beginner must be going through this thought, I am pretty much sure about it.
I have been around this thing for few hours now!!!!But I don't know how to use it and what is the difference between them?
function(d){return d}
function(d,i){return d and some more custom code}
for Example--->
var data = [4, 8, 15, 16, 23, 42];
Function(d):::::
chart.selectAll("div")
.data(data)
.enter().append("div")
.style("width", function(d) { return d * 10 + "px"; })
.text(function(d) { return d; });
------------------------------------------------------------------------------------
Function(d*i):::::
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("y", function(d, i) { return i * 20; })
.attr("width", x)
.attr("height", 20);
Your example is a good illustrator of the difference between the two.
In the first example, only d is used. d represents the data associated with a given selection. In this case, an array of selected div elements is created, one for each element in the data array:
chart.selectAll("div")
.data(data)
.enter()
.append("div")
This not only creates an array of div elements, but associates data with each of those elements. This is done on a one-to-one basis, with each div corresponding to a single element in the data array. One is associated with '4', one with '8', and so on.
If I then go on to use .text(function(d){...}) on the array of selections, d will refer to the data associated with each selected div, so if I use the following method on my selections:
.text(function(d) { return d; });
Each of my divs will have text added, the value of which is d, or the data associated with the element.
When an array of selections is created, they are also given an index in the array. In your example, this corresponds to the position of their data in the data array. If your function requests both d and i, then i will correspond to this index. Going back to our divs, the div associated with '4' will have an index of '0', '8' will have an index of '1', and so on.
It's also important to note that the character used in the variable requested doesn't matter. The first variable in the function call is always the data, and the second is the index. If i used a method like
.text(function(cat,moose){ return( "data is: " + cat + " index is: " + moose)})
cat will correspond to the data of the selection, and moose will correspond to the index.
I hope that this example can help you. This is a complete web page where you can start playing:
<!doctype html>
<meta charset="utf-8">
<title>my first d3</title>
<body>
<script>
var data=[10,20,30,40];
var lis = d3.select("body")
.append("ul")
.selectAll("li")
.data(data)
lis.enter()
.append("li")
.text(function(d,i){ return "item n° "+i+" has value: "+d})
</script>
Basically d is the value of the data, and i is his index.
You can take a look of this example here: http://jsfiddle.net/M8nK8/
If you're talking about the callback functions you would pass to methods like .attr(), then the function is called for each item in the current selection, where the i gives you the index of the current item, but depending on what you're doing you might not care about the index. So although D3.js will always call your function with both arguments, if you don't actually need the second argument in a particular case your function need not declare it explicitly.
JavaScript lets you call any function with any number of arguments regardless of how many were explicitly included in the function declaration. If you call a function with fewer arguments than were defined the leftovers will get the value undefined. If you call a function with more arguments than were defined you can still access the additional ones from within the function by using the arguments object - or you can ignore them.
(Note: you should have a lowercase f in function().)

Categories

Resources