From what I understand the proper way to update d3 data is by passing it into the selection's .data()
.
The data that I obtain is not ordered, or consistant for that matter. So I heavily need to use the update / add / remove logics. So in d3 terms that's .data()
with .enter()
and .exit()
(right?).
So what I'd like to do is to use a javascript dictionary instead of an array, where the key is my unique identifier. But I can't seem to do that. A fake example:
data_one[0] = 'Dogs';
data_one[1] = 'Cats';
d3.selectAll('circle').data(data_one).enter().attr(...)
The second time I run this my data may be same but in different order. I want to represent it with the same attributes as before. I don't want to duplicate my code, but if I just do .data(data_two) then wrong circles get updated with the new data.
Any way around this?
This is the purpose of the key function, the second argument to selection.data: it lets you maintain object constancy by controlling which datum gets bound to which element. For example, say you had an array of objects representing fruit:
var fruits = [
{name: "orange", value: 200},
{name: "apple", value: 124},
{name: "banana", value: 32}
];
When you first create the elements, you don’t need a key function because there aren’t any existing elements, and so you can use the standard selectAll-data-enter-append pattern:
var div = d3.select("body").selectAll(".fruit")
.data(fruits)
.enter().append("div")
.attr("class", "fruit")
.text(function(d) { return d.name + ": " + d.value; });
The result of this operation is:
<body>
<div class="fruit">orange: 200</div>
<div class="fruit">apple: 124</div>
<div class="fruit">banana: 32</div>
</body>
But now let’s say your data changes, and you want to update to reflect the new data. This new data may be in a different order, and some elements may be added or removed:
var fruits2 = [
{name: "apple", value: 124},
{name: "lemon", value: 17},
{name: "banana", value: 32},
{name: "strawberry", value: 1465}
];
The apple and banana were in the original data, and so we want to keep them. This is called the update selection. The strawberry and lemon are new; this is the enter selection. And lastly the orange went away, forming the exit selection.
Using a key function that refers to the name
property of the datum, we can assign the new data to the old elements as intended. Then we can create the entering fruit and remove the exiting fruit:
var div = d3.select("body").selectAll(".fruit")
.data(fruits2, function(d) { return d.name; });
div.enter().append("div")
.attr("class", "fruit")
.text(function(d) { return d.name + ": " + d.value; });
div.exit().remove();
This is called the general update pattern. (You could have done this the first time around; it’s the more general form of the selectAll-data-enter-append pattern.) The result is:
<body>
<div class="fruit">apple: 124</div>
<div class="fruit">banana: 32</div>
<div class="fruit">lemon: 17</div>
<div class="fruit">strawberry: 1465</div>
</body>
While the order of the selection div
matches the data fruit2
, the order in the DOM does not because the entering elements were appended to the end of the body. (selection.append doesn’t have any fancy logic to guarantee ordering; it just appends new elements to the end of the parent.) When using SVG, we often don’t care about the order of DOM elements because everything is positioned absolutely. If you do care about the order, you can use selection.order to fix it after entering:
div.order(); // make the DOM element order match the selection
Also, in this example, we don’t need to make any changes to the update selection, because the values for apple and banana didn’t change. If the updating data also changes, you can make chain calls to selection.attr and selection.text directly off of the selection.data call above.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With