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.
Related
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).
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)
I'd like to create a series of dl tags in a list from some data using d3.js.
The code I came up with is this:
var x=d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.selectAll()
.data(d=>Object.entries(d.volumeInfo)).enter();
x.append('dt')
.text(d=>d[0]);
x.append('dd')
.text(d=>d[1]);
where data is an array of objects. Everything works except the elements are not in the correct order.
Here is the order I manage to get:
<dl>
<dt>key1</dt>
<dt>key2</dt>
<dd>value1</dd>
<dd>value2</dd>
</dl>
But it should be like this:
<dl>
<dt>key1</dt>
<dd>value1</dd>
<dt>key2</dt>
<dd>value2</dd>
</dl>
I've done a fair amount of googling and nothing answers the question, at least not in a way that works in v5 or not with more than one dt/dd pair.
This seems like something basic that d3.js should be able to do.
In your solution:
x.append('dt')
.text(d=>d[0]);
x.append('dd')
.text(d=>d[1]);
All elements appended with an enter().append() cycle are appended to the parent, in the order they are appended, which for you runs like this: first all the dts, then all the dds, as you have seen. The placeholder nodes (these are not the appended elements) created by the enter statement do not nest children in a manner it appears you might expect them to.
Despite the fact that d3 doesn't include methods to achieve what you are looking for with methods as easy as a simple selection.append() method, the desired behavior can be achieved fairly easily with standard d3 methods and an extra step or two. Alternatively, we can build that functionality into d3.selection ourselves.
For my answer I'll finish with an example that uses your data structure and enter pattern, but to start I'll simplify the nesting here a bit - rather than a nested append I'm just demonstrating several possible methods for appending ordered siblings. To start I've also simplified the data structure, but the principle remains the same.
The first method might be the most straightforward: using a selection.each() function. With the enter selection (either with a parent or the entered placeholders), use the each method to append two separate elements:
var data = [
{name:"a",description:"The first letter"},
{name:"b",description:"The second letter"}
];
d3.select("body")
.selectAll(null)
.data(data)
.enter()
.each(function(d) {
var selection = d3.select(this);
// append siblings:
selection.append("dt")
.html(function(d) { return d.name; });
selection.append("dd")
.html(function(d) { return d.description; })
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, perhaps a more elegant option is to dig into d3.selection() and toy with it to give us some new behaivor. Below I've added a selection.appendSibling() method which lets you append a paired sibling element immediately below each item in a selection:
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
It takes each node in a selection, creates a new paired sibling node (each one immediately after the original node in the DOM) of a specified type, and then places the new nodes in a d3 selection and binds the data. This allows you to chain methods onto it to style the element etc and gives you access to the bound datum. See it in action below:
// modify d3.selection so that we can append a sibling
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
var data = [
{name:"a",description:"The first letter"},
{name:"b",description:"The second letter"}
];
d3.select("body")
.selectAll(null)
.data(data)
.enter()
.append("dt")
.html(function(d) { return d.name; })
.appendSibling("dd") // append siblings
.html(function(d) { return d.description; }) // modify the siblings
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Of course it is probably wise to keep the siblings in separate selections so you can manage each one for updates/entering/exiting etc.
This method is very easily applied to your example, here's a nested solution using data that is structured like you expect and the appendSibling method:
// modify d3.selection so that we can append a sibling
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
var data = [
{volumeInfo: {"a":1,"b":2,"c":3}},
{volumeInfo: {"α":1,"β":2}}
]
var items = d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.selectAll()
.data(d=>Object.entries(d.volumeInfo)).enter();
var dt = items.append("dt")
.text(function(d) { return d[0]; })
var dd = dt.appendSibling("dd")
.text(function(d) { return d[1]; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Here is a possibility using the .html appender (instead of .append):
var data = [
{ "volumeInfo": { "key1": "value1", "key2": "value2" }, "some": "thing" },
{ "volumeInfo": { "key3": "value3", "key4": "value4", "key5": "value5" } }
];
d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.html( function(d) {
// Produces: <dt>key1</dt><dd>value1</dd><dt>key2</dt><dd>value2</dd>
return Object.entries(d.volumeInfo).map(r => "<dt>" + r[0] + "</dt><dd>" + r[1] + "</dd>").join("");
});
dt { float: left; width: 100px; }
dd { margin-left: 100px; }
<script src="https://d3js.org/d3.v5.min.js"></script>
which produces this tree:
<ol>
<li>
<dl>
<dt>key1</dt>
<dd>value1</dd>
<dt>key2</dt>
<dd>value2</dd>
</dl>
</li>
<li>
<dl>
<dt>key3</dt>
...
</dl>
</li>
</ol>
Note that this is not exactly in the spirit of d3 and makes it difficult to work with appended children (adding class, style, other children, ...).
Example for data would be useful. But instead of appending to x, have you tried appending directly to dt itself?
x.append('dt').text(d=>d[0]).append('dd')
I'm trying to follow what Mike Bostock seems to indicate is a best practice, namely assigning your selectAll() to a variable and then separating out the update, the enter() and the exit(), but I'm noticing a discrepancy in V4 that I can't explain.
Consider the following working code:
// Bind an array of users to the #users div
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users))
// Add LIs for any new users
.enter()
.append("li");
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
Which creates an empty LI and then appends a SPAN inside it with the user name. (By the way, if this is all I wanted I'm sure there's a better way, but this is just a reduction of something else to illustrate my point. Bear with me if you will.)
Now I try to adapt it to my interpretatioin of said best practice by running the enter() against the stored variable, and the child elements lose their link to the parent data. I get a list of empty LIs.
// Bind an array of users to the #users div
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li");
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
Update 1
Well, after reading [~Gerardo Furtado]'s response, I thought I had it. Clearly I'm missing a fundamental principle here.
Here's my code that attempts to use merge() to make sure the data is carried over to the child elements, with no joy:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li")
// New for V4, merge back the original set to get the data
.merge(userNodes);
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
I suppose you're using d3 v4.x. In that case, that's the expected behaviour.
This is what's happening: userNodes is the data binding variable:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
Then, you write:
userNodes.enter()
.append("li");
And that's the "enter" selection.
In d3 v3.x that enter selection magically modifies your original variable, turning it into this:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
.enter()
.append("li");
However, in d3 v4.x, your original variable remains just a data binding selection:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
So, if we rewrite your userMessageGraph taking into account the chaining, this is what it really is:
var userMessageGraph = d3.select("#users")//here userNode starts
.selectAll("li")
.data(d3.values(users))//this is where userNode ends
.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
You can see that the "enter" selection for the <li>, which is...
.enter()
.append("li")
...is missing.
EDIT: This edit addresses the OP's new code:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li")
// New for V4, merge back the original set to get the data
.merge(userNodes);
That won't work for a reason: the merge function...
...returns a new selection merging this selection with the specified other selection. The returned selection has the same number of groups and the same parents as this selection. (emphasis mine)
As userNodes is an empty selection, this will not work. You can invert the logic:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
var userNodesEnter = userNodes.enter()
.append("li");
var userNodesUpdate = userNodesEnter.merge(userNodes);
var userMessageGraph = userNodesUpdate.selectAll("span")
.data(function(d) {
return [d.name];
})
.enter().append("span")
.text(function(d) {
return d;
});
I'm reading through the D3.js documentation, and am finding it hard to understand the selection.data method from the documentation.
This is the example code given in the documentation:
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; });
I understand most of this, but what is going on with the .data(function(d) { return d; }) section of the var td statement?
My best guess is as follows:
The var tr statement has bound a four-element array to each tr node
The var td statement then uses that four-element array as its data, somehow
But how does .data(function(d) { return d; }) actually get that data, and what does it return?
When you write:
….data(someArray).enter().append('foo');
D3 creates a bunch of <foo> elements, one for each entry in the array. More importantly, it also associates the data for each entry in the array with that DOM element, as a __data__ property.
Try this:
var data = [ {msg:"Hello",cats:42}, {msg:"World",cats:17} ];
d3.select("body").selectAll("q").data(data).enter().append("q");
console.log( document.querySelector('q').__data__ );
What you will see (in the console) is the object {msg:"Hello",cats:42}, since that was associated with the first created q element.
If you later do:
d3.selectAll('q').data(function(d){
// stuff
});
the value of d turns out to be that __data__ property. (At this point it's up to you to ensure that you replace // stuff with code that returns a new array of values.)
Here's another example showing the data bound to the HTML element and the ability to re-bind subsets of data on lower elements:
The key to understanding what this code is doing is to recognize that selections are arrays of arrays of DOM elements. The outer-most array is called a 'selection', the inner array(s) are called 'groups' and those groups contain the DOM elements. You can test this by going into the console at d3js.org and making a selection like d3.selectAll('p'), you will see an array containing an array containing 'p' elements.
In your example, when you first call selectAll('tr') you get a selection with a single group that contains all the 'tr' elements. Then each element of matrix is matched to each 'tr' element.
But when you call selectAll('td') on that selection, the selection already contains a group of 'tr' elements. This time each of those elements will each become a group of 'td' elements. A group is just an array, but it also has a parentNode property that references the old selection, in this case the 'tr' elements.
Now when you call data(function(d) { return d; }) on this new selection of 'td' elements, d represents the data bound to each group's parent node. So in the example, the 'td's in the first group will be bound with the array [11975, 5871, 8916, 2868]. The second group of 'td's are bound with [ 1951, 10048, 2060, 6171].
You can read mike bostock's own excellent explanation of selections and data binding here: http://bost.ocks.org/mike/selection/
Use the counter i to show the index of the data being used.
var tr = d3.select("body").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr") //create a row for each data entry, first index
.text(function(d, i) { return i}); // show the index i.e. d[0][] then d[1][] etc.
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.style("background-color", "yellow") //show each cell
.text(function(d,i) { return i + " " + d; }); // i.e d[from the tr][0] then d[from the tr][1]...