This may be a silly question, but I just can't figure out when to use .join and when to use .append in D3.
Like in this block, I don't know why to use join rather than append here.
elements.selectAll('circle')
.data(d=>d.values)
//.append('circle')
.join('circle')
.attr("class","dots")
.attr('r',2)
.attr('fill',d=>colorScale(d['track']))
.attr("cx", d=>dateScale(d['edate']))
.attr("cy", d=>valueScale(d['record_time']));
Can anybody help me to understand that?
TL;DR
selection.append by itself merely appends a single child element to every element in the selection it is called on (inheriting its parent's datum). selection.join() is data dependent: it conducts an enter/update/exit cycle so that the number of matching elements in the DOM matches the number of data array items.
The code you have suggests that you want to use .enter().append("circle") as opposed to merely .append("circle"): this completes the enter() portion of the enter/update/exit cycle that is also completed by using .join().
You can use join or individual enter/exit/update selections to achieve the same results, join is just a convenience, as stated in the docs:
This method is a convenient alternative to the explicit general update
pattern, replacing selection.enter, selection.exit, selection.append,
selection.remove, and selection.order. (docs)
Enter/Update/Exit
When you see selectAll() followed by .data() we are selecting all matching elements, for each one that exists, we bind an item from the data array to it. The use of .data() returns what is called the update selection: it contains existing elements (if any) with the newly supplied data bound to those existing items.
However, if the number of selected elements does not match the number of items¹, then .data() creates an enter selection or an exit selection. If we have excess data items, then we have an enter selection with one element for every item we need to add in order to have an equal number of DOM elements and data array items. Conversely, if we have excess DOM elements, then we have an exit selection.
Calling .enter() on the update selection returns the enter selection. This selection contains placeholders ("Conceptually, the enter selection’s placeholders are pointers to the parent element docs"), which we can use .append("tagname") with to add the elements we need.
Conversely, calling .exit() on the update selection returns the exit selection, which often is simply removed with .exit().remove();
This pattern generally looks something like this:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
circle.exit().remove();
circle.enter()
.append("circle")
.attr...
circle.attr(...
First we select the all the circles, let's say there are 2 circles to select.
Second we remove excess elements using selection.exit() : however, since we have four data items and only two matching DOM elements there is nothing to remove, so the selection is empty and nothing is removed.
Third we add elements as required to ensure that the number of matching DOM elements is the same as the number of data array items. As we have four data items and only two matching DOM elements the enter selection contains two placeholders (pointers to the parent). We append circles to them and style them as we want.
Lastly we use the update selection containing the two pre-existing circles and style them as we want based on the new data.
Often we want to style new elements and existing elements the same, so we could use the merge method to combine the enter and update selections:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
circle.exit().remove();
circle.enter()
.append("circle")
.merge(circle)
.attr(...
This simplifies our code a bit as we don't need to duplicate styling for both enter and update separately.
Join
So where does .join() come in? Its for convenience. In its simplest form: .join("elementTagName") .join replaces the above code with:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
.join("circle")
.attr(...
Here the join method removes the exit selection and returns a merged update selection and enter selection (containing new circles), which we can now style as needed. It is essentially a short hand method that allows you to write more concise code, but is functionally the same² as the 2nd code block.
Your Code
In your code, you have a selection of one or more elements (a/some parent element/s). The bound datum contains a child array, which you wish to use to create child elements. In order to do so you provide to .data() that child data array:
parentElements.selectAll("circle")
.data(d=>d.values)
You can follow that up with .join(): this will do the enter/update/exit cycle for every parent element so that they each have the proper amount of circles and returns a selection of all these circles.
You cannot use just .append() because that would append one circle to every parent element, returning a selection of these circles. This is very unlikely to be the desired result.
Instead, as noted at the top of this answer, you can use .enter().append("circle") so that you are using the pattern correctly.
You only need the enter selection if you create the elements once and never update the data, otherwise, you'll need to use enter/update/exit methods to handle excess elements, excess data items, and updated elements.
Ultimately, the difference between join and enter/update/exit is a matter of code preference, style, succinctness, but otherwise, there is nothing that you can't do with one that you can't do with the other.
¹ Assuming the provision of only one parameter to .data() - the 2nd, optional, parameter is a keying function which matches DOM element and data item based on a key. DOM elements without a matching datum are placed in the exit selection, data array items without a matching DOM element are placed in the enter selection, the remainder are placed in the update selection.
² Assuming that .join() is not provided its second or third parameters, which allow more granular control of the enter/exit/update cycle.
I've build the following table here :
https://bl.ocks.org/simonbreton/d4d2ea338d1bacc6ce3d0a295529bcb4
However as you can see if you try to select different option, data doesn't update correctly.
Some doesn't show up (here in the picture batman for example) and others doesn't remove. For exemple in the picture again, I've expected to remove superman, captain and Antman for seeing only batman data.
What's wrong with my code ?
thanks a lot !
By default d3 identifies data by index in the array. It doesn't play well with filtering - some elements get dropped and it changes the indexes of succeeding elements. Adding a key function to your data binding could help, however, there is also a problem with your dataset. It contains duplicates and there is no property that could distinguish them. If names were unique you could use a key function like below:
var rows = tbody.selectAll("tr")
.data(filterdata, function (d) { return d.name})
With d3 the selection returned by *.enter() is special in that it is only a placeholder for coming elements. Unfortunately this means I can not get the data related to the entering elements using *.data() (as is possible with *.exit().data()).
I'm currently in a situation where the timing of several transitions is dependant on the content of the entering elements before these elements are initialised.
My question is thus: How do I obtain an array of the data objects that will be linked to the entering elements in a data join, before these have been instantiated?
You can access the data structures inside the selection directly. At the top level, there's a single element array. The element contains the placeholder elements with the data bound to them for the enter selection. You just need to iterate over those elements.
var enterData = selection.data(data)
.enter()[0].map(function(d) { return d.__data__; });
Complete demo here.
I am using this RGraph example from the InfoVis toolkit to draw my nodes. This is how my nodes look in JSON:
{"id":"parentId","name":"parent","adjacencies":[{"nodeTo":"missingChildId","nodeFrom":"parentId"}]}
The problem is that missingChildId refers to a non-existing node. Currently InfoVis draws an edge from the parent node to a node which it labels "missingChildId".
I don't want this edge to be drawn.
Similarily the function node.eachAdjacency gives nodes that don't exist in the graph. Is there some sort of filter to sort those missing nodes out?
Thank you.
There seems to be a couple of problems with your json data.
1: If you specify "nodeFrom" and "nodeTo" in adjacencies in your json data then infovis will create those adjacencies. In ideal case you should not have such adjacencies in your json data.
2: Also, nodeTo and nodeFrom in adjacencies points to ids of the nodes you want to refer to and ids are supposed to be unique. From your data it seems like "missingChildId" and "parentId" are not unique. Are you sure those are unique ids?
I think you must make sure that each node has a unique id and use them in adjacencies.
If there is no way you can fix your json for 1st problem then one work around is to hide those nodes with id "missingChildId".
So, after your graph is rendered, you can use following code to hide nodes with id "missingChildId".
rg.graph.eachNode( function(node){
if( node.id == "missingChildId" )
node.setData("alpha",0,"end");
});
rg.graph.animate({
modes: ['node-property:alpha'],
duration: 500
});
Similarly every time you iterate over nodes/adjacencies of graph you will have to filter out all such unwanted nodes by using their id.
You can also set your custom property on nodes which are unwanted.
node.setData("ignore",true);
And then filter them using this property.
So I have two sets of data, lets call them A and B and then one canvas X
So first I do this:
var selection=canvas.selectAll("circle")
.data(A)
selection.enter().append("circle")
selection
.attr(...)
.attr(...)
So this does what I want and makes my first group of circles. But then I want to make a second group of circles with different properties based on data B so I tried doing the following:
var selection2 = canvas.selectAll("circle")
selection2.enter().append("circle").data(B)
selection2
.attr(...)
.attr(...)
however this doesn't seem to work and selection2 interferes with selection one. How do I get this to work?
just add them as elements with a different selector... e.g.
var selection2 = canvas.selectAll(".circleb")
selection2.enter().append("circle").data(B)
selection2
.classed(".circleb")
.attr(...)
.attr(...)
With your current approach, you're selecting all circle elements (which are currently bound to dataset A), and trying to rebind them to dataset B. By using a different selector (as per my example) you create a second set of elements bound to dataset B, leaving the original elements bound to dataset A.
edited to fix a night-time blooper (as pointed out in the comments)