Skip to content

Commit 6d5698d

Browse files
committed
update data binding tutorial content to explain selection.join
1 parent e0483cc commit 6d5698d

File tree

1 file changed

+112
-96
lines changed

1 file changed

+112
-96
lines changed

z03-data-binding.md

Lines changed: 112 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ permalink: /data-binding/
55
---
66

77
- [Selections `d3.selectAll`](#selections-d3selectall)
8-
- [Joins `selection.data()`](#joins-selectiondata)
9-
- [Adding Elements `selection.enter()`](#adding-elements-selectionenter)
10-
- [Removing Elements `selection.exit()`](#removing-elements-selectionexit)
8+
- [Joins `selection.data()` and `selection.join()`](#joins-selectiondata-and-selectionjoin)
9+
- [Adding Elements](#adding-elements)
10+
- [Removing Elements](#removing-elements)
1111
- [Identity and the Key Function](#identity-and-the-key-function)
1212
- [Transitions `selection.transition()`](#transitions-selectiontransition)
1313

14-
D3 selections are a different way to look at data binding. They're powerful
15-
because the same selection can be updated for different data later on. Updating
16-
is the most powerful part of selections.
14+
D3 selections are a different way to look at data binding. Essentially, D3 maintains a mapping of data points to DOM elements, keeping track of exactly which data maps to which element. When data points are added, changed, or removed, the associated DOM elements can be programmatically added, updated, or removed correspondingly. This feature is extremely powerful, and allows you to add many different kinds of custom interactivity in your visualizations.
15+
16+
<div class="info">
17+
Note: The `selection.join` API used in this tutorial is only available to D3 v5 and later. For the older data binding pattern, please refer to previous versions of this tutorial.
18+
</div>
1719

1820
## Selections `d3.selectAll`
1921

20-
Ok, so we've referenced `d3.select()` and `d3.selectAll()` a few times already
22+
We've referenced `d3.select()` and `d3.selectAll()` a few times already
2123
but now, it's really time to dig in. `d3.select()` will find one element,
2224
`d3.selectAll` will match all available elements.
2325

@@ -30,7 +32,7 @@ d3.select(String selector) -> (d3.selection)
3032
D3 selections are a group of elements that match a query **or could match a
3133
query later** (the elements may not have been constructed yet).
3234

33-
## Joins `selection.data()`
35+
## Joins `selection.data()` and `selection.join()`
3436

3537
Selections are used to map pieces of our data to elements in the DOM. Suppose we
3638
have some data:
@@ -83,7 +85,7 @@ svg.size();
8385
// 1 -- one <svg> element exists
8486

8587
var rects = svg.selectAll('rect')
86-
.data(sales);
88+
.data(sales);
8789

8890
rects.size();
8991
// 0 -- no <rect> elements exist yet!
@@ -93,7 +95,26 @@ rects.size();
9395

9496
Okay, now we have a selection but still no elements! We have more work to do.
9597

96-
## Adding Elements `selection.enter()`
98+
The `selection.join()` API allows us to define what happens when we join data with a selection. In other words, we use this API to define how to handle additions, changes, or removals to the data since the last join.
99+
100+
The `selection.join()` API takes 3 functions as arguments:
101+
- the first function will be called with a selection containing data points which do not have DOM elements yet
102+
- the second function will be called with a selection which contains all the data points
103+
- the third function will be called with a selection which contains data points which have been removed, but for which DOM elements still exist.
104+
105+
The second and third arguments are optional. This can be a bit confusing at first, but don't worry. Continue reading, and I'll explain how all this works through examples. Feel free to reference the [official documentation](https://github.com/d3/d3-selection#selection_join).
106+
107+
<div class="info">
108+
In D3 selections, "enter" refers to data points which do not have a corresponding DOM element (data that was added since the last join) and "exit" refers to DOM elements which do not have a corresponding data point (data that was removed since the last join).
109+
<br>
110+
<br>
111+
The `selection.enter()` and `selection.exit()` method of selections can be used to access these subsets - that was how we handled additions and removals before the `selection.join()` API existed.
112+
<br>
113+
<br>
114+
Now, the "enter" and "exit" selections are automatically passed to the first and third arguments of `selection.join()` - we just need to provide functions to handle them.
115+
</div>
116+
117+
## Adding Elements
97118

98119
Again, our goal is to have a rectangle for each data point. We are starting with
99120
none and we have 4 new data points, so obviously the right thing to do is to
@@ -107,39 +128,33 @@ the next selection, things will be more complex since there will already be
107128
rects.
108129

109130
The part of a D3 selection that represents these element-less data-points
110-
is `selection.enter()`;
111-
112-
<div class="example-row-1"> <div class="example">
113-
{% highlight javascript %}
114-
var newRects = rects.enter();
115-
{% endhighlight %}
116-
</div>
117-
</div>
118-
119-
So now `newRects` represents these element-less data-points, so we use
120-
`append` to add new elements. The elements don't add themselves, we have to
131+
is passed to the first argument in `selection.join`. The elements don't add themselves, we have to
121132
create the elements that will match the selection ourselves. We use the same
122133
attribute editing helpers to configure each circle per its data point.
123134

124-
125135
<div class="example-row-2">
126136
<div class="example">
127137
{% highlight javascript %}
128138
// recall that scales are functions that map from
129139
// data space to screen space
130140
var maxCount = d3.max(sales, (d, i) => d.count);
131141
var x = d3.scaleLinear()
132-
.range([0, 300])
133-
.domain([0, maxCount]);
142+
.range([0, 300])
143+
.domain([0, maxCount]);
134144
var y = d3.scaleOrdinal()
135-
.rangeRoundBands([0, 75])
136-
.domain(sales.map((d, i) => d.product));
137-
138-
newRects.append('rect')
139-
.attr('x', x(0))
140-
.attr('y', (d, i) => y(d.product))
141-
.attr('height', y.rangeBand())
142-
.attr('width', (d, i) => x(d.count));
145+
.rangeRoundBands([0, 75])
146+
.domain(sales.map((d, i) => d.product));
147+
148+
rects.join(
149+
// NEW - handle data points w/o rectangles
150+
newRects => {
151+
newRects.append('rect')
152+
.attr('x', x(0))
153+
.attr('y', (d, i) => y(d.product))
154+
.attr('height', y.rangeBand())
155+
.attr('width', (d, i) => x(d.count));
156+
},
157+
);
143158
{% endhighlight %}
144159
</div>
145160

@@ -186,31 +201,41 @@ So how does it turn out? Let's take a look:
186201
style of <kbd>(d, i)</kbd> parameters to represent the element and its index.
187202
</div>
188203

189-
## Removing Elements `selection.exit()`
204+
## Removing Elements
190205

191-
Where `selection.enter()` selects elements that have added since the last data
192-
join, `selection.exit()` is the opposite, it applies to elements that have been
206+
Whereas "enter" selects elements that have added since the last data
207+
join, "exit" is the opposite, it applies to elements that have been
193208
removed.
194209

195210
Suppose we drop the first point from our source array, we can find and operate
196-
on the corresponding element in the DOM via `selection.exit()`.
211+
on the corresponding element in the DOM via the exit selection.
197212

198-
We can use the `remove()` method to immediately delete matched elements, it's
199-
the opposite of `append()`.
213+
We can use the `remove()` method to immediately delete matched elements; it's
214+
the opposite of `append()`.
215+
216+
If you only want to delete matched elements, you may omit the argument entirely from `selection.join()` since calling `remove()` is the default behavior.
200217

201218
<div class="example-row-2">
202219
<div class="example">
203220
{% highlight javascript %}
204-
sales.pop(); // drops the last element
205-
206-
var rects = rects.data(sales); // join the data again
207-
208-
var rectsToRemove = rects.exit();
209-
210-
rectsToRemove.size()
211-
// 1 -- one element is part of the exit selection
221+
// define new logic for handling joins
222+
rects.join(
223+
newRects => {
224+
newRects.append('rect')
225+
.attr('x', x(0))
226+
.attr('y', (d, i) => y(d.product))
227+
.attr('height', y.rangeBand())
228+
.attr('width', (d, i) => x(d.count));
229+
},
230+
rects => {},
231+
// NEW - delete elements whose data has been removed
232+
rectsToRemove => {
233+
rectsToRemove.remove();
234+
}
235+
);
212236

213-
rectsToRemove.remove(); // instantly removes
237+
sales.pop(); // drops the last element
238+
rects.data(sales); // join the data again
214239
{% endhighlight %}
215240
</div>
216241

@@ -245,7 +270,7 @@ obj2 == obj3;
245270
</div>
246271
</div>
247272

248-
But the example with `selection.exit()` above works! It only removed one element
273+
But the example above works! It only removed one element
249274
from the DOM because we only removed one element from the array, and all the
250275
rest of the objects were the exact same.
251276

@@ -273,21 +298,13 @@ var sales2 = [
273298
];
274299

275300
var rects = svg.selectAll('rect')
276-
.data(sales1, (d, i) => d.product);
277-
278-
rects.enter().append('rect');
279-
280-
rects.size();
281-
// 2 -- first join, adds two new elements
282-
283-
var nextrects = rects
284-
.data(sales2, (d, i) => d.product);
301+
.data(sales1, (d, i) => d.product)
302+
.join(enter => enter.append("rect"));
285303

286-
nextrects.exit().size();
287-
// 1 -- one element to remove
288-
nextrects.exit().remove();
304+
rects.size(); // 2 -- first join adds two new elements
289305

290-
nextrects.enter().append('rect'); // adds one element
306+
// removes 1 element, adds 1 element
307+
rects.data(sales2, (d, i) => d.product);
291308
{% endhighlight %}
292309
</div>
293310
</div>
@@ -370,27 +387,25 @@ We can use transitions to demonstrate this update.
370387
<div class="example">
371388
{% highlight javascript %}
372389
function toggle() {
373-
sales = (sales == days[0]) ? days[1] : days[0];
374-
update();
390+
sales = (sales == days[0]) ? days[1] : days[0];
391+
update();
375392
}
376393

377394
function update() {
378-
var rects = svg.selectAll('rect')
379-
.data(sales, (d, i) => d.product);
380-
381-
// When we enter, we add the DOM element
382-
// and set up the things that won't change
383-
var enterRects = rects.enter()
384-
.append('rect')
385-
.attr('x', x(0))
386-
.attr('y', (d, i) => y(d.product))
387-
.attr('height', y.bandwidth())
388-
389-
// "rects" represents the update selection, we need to
390-
// manually merge it with the enter selection to update
391-
// all rects at the same time
392-
rects.merge(enterRects)
393-
.attr('width', (d, i) => x(d.count));
395+
svg.selectAll('rect')
396+
.data(sales, (d, i) => d.product)
397+
.join(
398+
enter => {
399+
enter.append('rect')
400+
.attr('x', x(0))
401+
.attr('y', (d, i) => y(d.product))
402+
.attr('height', y.bandwidth())
403+
.attr('width', (d, i) => x(d.count));
404+
},
405+
update => {
406+
update.attr('width', (d, i) => x(d.count));
407+
},
408+
);
394409
};
395410
{% endhighlight %}
396411
</div>
@@ -415,27 +430,28 @@ are pretty nice.
415430
<div class="example">
416431
{% highlight javascript %}
417432
function toggle() {
418-
sales = (sales == days[0]) ? days[1] : days[0];
419-
update();
433+
sales = (sales == days[0]) ? days[1] : days[0];
434+
update();
420435
}
421436

422437
function update() {
423-
var rects = svg.selectAll('rect')
424-
.data(sales, (d, i) => d.product);
425-
426-
var enterRects = rects.enter()
427-
.append('rect')
428-
.attr('x', x(0))
429-
.attr('y', (d, i) => y(d.product))
430-
.attr('height', y.bandwidth())
431-
.attr('width', (d, i) => x(d.count));
432-
433-
rects.merge(enterRects)
434-
.transition() // NEW
435-
.duration(1000) // Also NEW
436-
.attr('width', (d, i) => x(d.count));
438+
svg.selectAll('rect')
439+
.data(sales, (d, i) => d.product)
440+
.join(
441+
enter => {
442+
enter.append('rect')
443+
.attr('x', x(0))
444+
.attr('y', (d, i) => y(d.product))
445+
.attr('height', y.bandwidth())
446+
.attr('width', (d, i) => x(d.count));
447+
},
448+
update => {
449+
// NEW!
450+
update.transition().duration(1000)
451+
.attr('width', (d, i) => x(d.count));
452+
},
453+
);
437454
};
438-
439455
{% endhighlight %}
440456
</div>
441457

0 commit comments

Comments
 (0)