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).
Related
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)
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.
when I create circles in D3 i can assign them e.g. onclick events
svg.selectAll("circle").data(dataArray).enter().append("circle").on("click"),function(d){// do stuff});
When I want to create new circles and therefore update the data set of my circles I do this:
svg.selectAll("circle").data(newDataSet,function(d){return d;}).enter().append("circle")
(I left the attributes out on purpose)
Is there a way to somehow inherit the on() events from my old circles or do I have to define these events again?
From my understanding it shouldn't be possible, because d3 is not object orientated.
Firstly, below is not entirely correct. enter will just give you nodes for which there is no existing DOM element. You would use merge (in v4) to update the already existing nodes.
When I want to create new circles and therefore update the data set of
my circles I do this:
svg.selectAll("circle").data(newDataSet,function(d){return d;}).enter().append("circle")
Coming to your actual question, below would assign click listener for each of the circle dom node.
svg.selectAll("circle").data(dataArray).enter().append("circle").on("click", function(d){/* do stuff */});
So, new nodes added would have to assigned new event listeners.
I think you might be missing understanding of data joins, this is an excellent read. Using data joins would look something like this:
function makeCircles(data) {
var circles = d3.select('svg')
.selectAll('circle')
.data(data, function(d) {
return d;
});
circles.exit().remove();
circles.enter()
.append('circle')
.merge(circles)
.attr('r', function(d) {
return d + 5;
})
.attr('cx', function(d, i) {
return 50 + 50 * i;
})
.attr('cy', function(d, i) {
return 30;
})
.on('click', function() {
// do stuff
})
}
var data = [1, 2, 3];
makeCircles(data);
data = [10, 15, 16];
makeCircles(data);
If your concern is about assigning multiple event listeners, why not assign event listener to parent element of circles and let the events bubble?
Fairly new to d3.js, so hoping there's something obvious that I'm missing. Have been looking at this code over and over again, not sure where it's going wrong.
I have a bargraph, which displays 28 bars. I'm trying to:
Replicate this tutorial, where new data is added to graph, and oldest data is removed.
Instead of using shift to remove data, I'd like to push to the graph/array, but only display the last 28 numbers. I'd like to use the whole array for another display. This said, I can't get the above to work.
This is a jsFiddle to the troublesome code.
I have a graph located within a group (with a unique ID, #kI1data, plan on having multiple graphs later). When that group is clicked, a value from the data array is shifted, and another pushed. The graph is then redrawn. I believe it's this redraw function that is causing an issue; it removes rectangles, but doesn't add any new ones after the first click.
graphGroup.append("g")
.attr("id", "kInv1")
.append("rect")
.attr("class", "invBack")
.attr("x", 0).attr("y", 0)
.attr("width", kI1width).attr("height", kI1height)
.attr("fill", "grey")
.on("click", dataChange); // Add/remove data, and update graph
function dataChange() {
kInvd1.shift();
kInvd1.push(30); // 30 a placeholder number
updateGraph();
};
function updateGraph() {
// Select the group containing the rectangles that need updating
// (to avoid selecting other rectangles within the svg)
var kI3dataSelect = d3.select("#kI1data").selectAll("rect").data(kInvd1, function(d) { return d; });
// Enter new data
kI3dataSelect.enter().append("rect")
.attr("x", function(d, i) { return 1 + ((i+1) * ((kI1width)/28)); })
.attr("y", function(d) { return ykI1(d); })
.attr("height", function(d) { return (kI1height) - ykI1(d); })
.attr("width", (kI1width)/28)
.attr("fill", "black");
// Update positions (shift one bar left)
kI3dataSelect
.transition().duration(100)
.attr("x", function(d, i) { return 1 + (i * ((kI1width)/28)); });
kI3dataSelect.exit()
.transition().duration(100)
.attr("x", function(d, i) { return 1 + ((i-1) * ((kI1width)/28)); })
.remove();
};
For now I'm just trying to get the newly added data to display, whilst the oldest data is removed. Once that's done, if there are any pointers on how to display just the last 28 numbers in the array, it'd be very much appreciated!
The jsFiddle shows how a new bar is added on the first click, but subsequent clicks only translate and remove data, whilst new bars are not added.
Changing your dataChange function to the following gets you new insertions and limits the over all array to 28 elements.
function dataChange() {
if (kInvd1.length >= 28) kInvd1.shift()
kInvd1.push(Math.random() * 60); // 30 a placeholder number
updateGraph();
};
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().)