Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~19 min· topic 3 of 7

Topic 5.3

The Data Join, Shown on Plain Divs

In one line

The data join pairs each item of an array with one element. Items with no element are the enter group, pairs are the update group, and elements with no item are the exit group. Learn it on plain divs, where nothing else gets in the way.

Think of it like this

A party host with a guest list and a row of name tags on a table. Each guest on the list takes the next tag. If there are more guests than tags, the host makes new tags (enter). If a guest already has a tag, the host keeps it and checks it (update). If tags are left over because fewer guests came, the host throws them away (exit). The join is that host.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

data join
Pairing the items of an array with page elements, so the page can be updated to match the data.
enter
The items that have no element yet. They need new elements.
update
The items that matched an existing element. The element stays and gets the new item.
exit
The elements that have no item any more. They are usually removed.
__data__
The property D3 sets on an element to store the item bound to it.
datum
One item of data, such as one station's row. The plural is data.
merge
Joining the enter and update selections into one so you can style both together.

Step by step

01Bind, then look at the three groups

The first example calls show(data) five times on the same container. Each call binds, prints the sizes of the three groups, adds missing divs, writes the text and removes the extras.

Three station names: enter 3, update 0, exit 0, because the container starts empty. Five names: enter 2, update 3, exit 0. Two names: enter 0, update 2, exit 3. An empty array: enter 0, update 0, exit 2. One name: enter 1, update 0, exit 0 (the container was empty).

02Meet the stored datum

The last line prints root.select("div").node().__data__: the string Quay. The item lives on the element. When you call data() again, D3 replaces it. This is why attr("x", (d) => ...) always sees the latest item.

03Let join() do the sorting

The second example replaces the five lines with join(enter, update, exit). Enter boxes are teal, kept boxes yellow, exit boxes red. The exit function returns the selection without removing it, so you can see the red boxes: a join that does not remove its exit group leaves the old elements on the page, still showing their old text.

04Feed it bad data

The end of the second example gives data() an undefined, a null, a number, a string and an object. The first two throw, the number and the object become empty lists, and a string becomes its letters. The real error texts are in the output below.

Try it yourself

  1. 1

    Remove the exit group in join()

    In the second example change the exit function to (exit) => exit.remove(). The red boxes disappear and the count printed after the third call is 1, not 4. A plain join("div") does the same.

  2. 2

    Use a three-item array and one existing element

    In the scrubber, set the elements to Quay and the data to Quay, Market, Chapel. The Quay pair is update, Market and Chapel are enter, and nothing exits. Then swap the lists and the groups swap too.

Code & diagrams

The three groups, one call at a time New tab

The empty array is a normal case, not an error: every element exits and the chart is empty.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

data 3: enter 3, update 0, exit 0
  divs now: Quay Market Chapel
data 5: enter 2, update 3, exit 0
  divs now: Quay Market Chapel Library Lantern
data 2: enter 0, update 2, exit 3
  divs now: Library Lantern
data 0: enter 0, update 0, exit 2
  divs now:
data 1: enter 1, update 0, exit 0
  divs now: Quay
__data__: Quay
join() with a function per group, and bad data New tab

A failed data load leaves `undefined`: the join throws, so the chart is not drawn at all. Guard it where you load the data (Topic 4.2).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

enter x3, update x0, exit x0 | divs on the page: 3
enter x1, update x3, exit x0 | divs on the page: 4
enter x0, update x1, exit x3 | divs on the page: 4
red exit boxes still there: 3
undefined -> TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
null -> TypeError: Cannot use 'in' operator to search for 'length' in null
5 -> items: 0
"abc" -> items: 3
{} -> items: 0

Try it: interactive

Step through a join

Edit the data and the existing elements, then press the step buttons to watch update, enter and exit. Tick the key box and see which matches change.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Bind the data that failed to load

In the second example change show(["Library"]); to show(undefined);.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

Myth vs fact

Myth

enter is the elements that are entering.

Fact

At that point the elements do not exist. enter() holds placeholders for items that have no element yet. append is what creates them.

Myth

data() copies the data into the page.

Fact

It stores a reference to each item on its element as __data__. Change the item's object and the element sees it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    D3 pairs elements and items in groups. With nested selections (Topic 5.5) each parent node has its own group, and each group is joined on its own. A join never mixes children of different parents.

  • ▸

    The update selection that data() returns is a sparse array: the slots for enter items are empty (null). size() and nodes() skip empty slots, so with 5 items and 3 elements update.size() is 3 (as the first example prints), while enter().size() is 2.

Remember this

  1. 1

    The problem this solves: a chart's data changes (a new month, a filter, a live update). You need the page to match the new data: add what is new, keep what stays, remove what is gone. Doing this by hand is where Arjun's chart broke in the story. The join does the sorting into the three groups for you.

  2. 2

    **data() binds.** selectAll("div").data(array) pairs, by position, the first array item with the first existing div, the second with the second, and so on. The item is stored on the element as a property called __data__. You can see it in the console. That stored item is what d is in every later attr("x", (d) => ...).

  3. 3

    **Three groups come out of data(). The returned selection is the update** group: elements that found an item. .enter() is the items that found no element yet, as placeholders. .exit() is the elements that found no item. With 3 divs and 5 items: update 3, enter 2, exit 0. With 3 divs and 2 items: update 2, enter 0, exit 1.

  4. 4

    The old way spelled it out. update.enter().append("div") creates the missing elements, update.exit().remove() deletes the extra ones, and you had to merge enter and update with merge() to set attributes on both. The first example does it that way, so you see each group. You will meet this form in older code.

  5. 5

    **join() is the version 7 way.** selection.data(array).join("div") does all of it: it appends a div for every enter item, keeps the update elements, removes the exit elements, and returns enter and update together, ready for attr and text. When you need different behaviour per group (colour only new ones, keep exit elements for a fade-out in Phase 9), pass three functions: join(enter => ..., update => ..., exit => ...). The second example does this.

  6. 6

    Edge cases. An empty array: everything exits, nothing enters. One item and no elements: one enter. The array is undefined or null (a failed load): data() throws a TypeError and nothing is drawn, so check the data first. A number or an object with no length is treated as an empty list. A string becomes a list of its letters ("abc" gives three items).

  7. 7

    Matching is by position unless you give a key. The pairing so far is by index, which is right when the data only grows or shrinks at the end, and wrong when items reorder or leave from the middle. Topic 5.4 adds keys. The scrubber below lets you try both.

Explain it without notes

01

What are the enter, update and exit groups? Give the sizes for 3 divs and 5 items.

02

What does data() store on an element, and where?

03

What does join("div") do for you that enter().append("div") does not?

Practice

01

A <div id="tags"> already holds 4 spans. Write the D3 call that makes it show the names ["Quay", "Market"] with exactly two spans, and say which groups were involved.

02

Write showCount(data) that logs the sizes of enter, update and exit for a join on li elements in #list, then applies join("li") with the text from the data.

Trade-offs

  • ↔

    Index matching is simple and fast, and it is wrong whenever items leave or reorder in the middle. Keys (next topic) fix that at a small cost per update.

Done when you can

  • I can say which items go to enter, update and exit for any two sizes.

  • I can find the bound item in __data__.

  • I can write a join with join("div") and with three functions.

  • I know that data(undefined) throws and an empty array does not.