Understanding how D3.js binds data to nodes - javascript

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

Related

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

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)

How do I create a <dl> using d3.js

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

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.

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