Topic 5.4
Keys and Object Constancy
In one line
Without a key, the join pairs by position, so when the data reorders or an item leaves, the wrong element gets the wrong item. A key function, such as (d) => d.id, pairs by identity, so each station keeps its own element.
Think of it like this
A cloakroom. If the attendant hands back coats by position ("you are third in the queue, so you get the third coat on the rail"), people go home with the wrong coats whenever the queue changes order. A numbered ticket (a key) gives each person their own coat no matter where they stand. The key is the ticket.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- key
- A value that identifies one item, such as a station id. D3 uses it to decide which element belongs to which item.
- key function
- The function you give as the second argument of
data(). It returns the key of an item. - object constancy
- The same thing in the data keeps the same element on screen when the data changes.
- stable
- A key is stable when it does not change as the data changes. An id is stable, a rank is not.
- unique
- A key is unique when no two items share it.
- order()
- A selection method that moves elements in the page into the order of the selection.
Step by step
01Join June to July with no key
The first example draws three rows for June (Quay, Lantern, Library) and stamps each new element with el1, el2, el3. Then July arrives (Lantern, Quay, Chapel). By position, el1 now says Lantern, el2 says Quay, el3 says Chapel: the stamps are in the same places but the stations behind them changed. Everything that belonged to el1 now describes another station.
02Join again with a key
With (d) => d.id the July rows go in the order Lantern (el2), Quay (el1), Chapel (el4, a new element). Library left, so el3 was removed. Each station kept its element and the DOM was reordered to match.
03Break the keys on purpose
The second example counts enter, update and exit for six situations: a repeated key among the elements, a repeated key in the new data, numbers against strings, keys that are objects, a key function that reads the wrong field. The last two show the silent cases: the join always "works", but nothing is reused.
04Sort the rows
A keyed join with ["c", "a", "b"] moves the elements in the page to cab (that is order() inside join). Then .sort((a, b) => a.localeCompare(b)) puts them in abc. Sorting reorders the elements and leaves their data alone.
Try it yourself
- 1
Key by name instead of id
In the first example change both keyed
drawcalls to use(d) => d.name. The stamps come out the same, because the names are unique too. Now change the Chapel row of July to{ id: "S01", name: "Chapel" }and use the id key again: two items share the keyS01, the first keepsel1, and the repeat goes to enter, so the last line endsel4=S01. - 2
Key by position by accident
Change the key to
(d, i) => i. The result is the same as no key at all: Lantern lands onel1. A key that is the index has no identity in it.
Code & diagrams
Read the stamps: with no key the element `el1` shows Quay in June and Lantern in July. With the key, `el1` still shows Quay.
Preview
Console
Nothing logged yet.Expected console output
NO KEY (matched by position)
june: el1=S01 el2=S06 el3=S04
july: el1=S06 el2=S01 el3=S03
KEY (d) => d.id
june: el1=S01 el2=S06 el3=S04
july: el2=S06 el1=S01 el4=S03Each `report` puts elements on the page, then joins new data with the same key function and counts the groups. No line throws: these are the silent failures.
Preview
Console
Nothing logged yet.Expected console output
A repeated among the elements: enter 0, update 2, exit 1
A repeated in the new data: enter 1, update 2, exit 0
numbers 1,2,3 <- strings '1','2','4': enter 1, update 2, exit 1
key returns the object itself: enter 2, update 1, exit 1
key reads a field the data lacks: enter 2, update 0, exit 2
order after join: cab
after sort: abcBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Key on a field that does not exist
In the second example's last report, the key (d) => d.id reads id from rows that have station instead.
Myth vs fact
Myth
The join works out which bar is which, even with no key.
Fact
With no key, it pairs by position only. Reordered or removed items end up on the wrong elements.
Myth
A key must be a string.
Fact
A key can be a number: D3 converts it to a string, so 1 and "1" are the same key. It cannot be an object, because every object becomes "[object Object]".
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The key function receives
(d, i, nodes). It is called once for each existing element's bound datum and then once for each new item, so it runselements + itemstimes per update. Keep it a cheap property read. - ▸
Which key to choose is a design decision. For a time series of days, the date string is a good key. For a map of 25 stations it is the id. For a chart of the top 10, the key is the entity, not the rank: when Quay moves from 3rd to 1st you want its bar to move, not to be recreated.
Remember this
- 1
The problem this solves: in Phase 5's story the chart for June lists stations in one order, and July lists them in another. By position, the bar that showed Quay now shows Lantern's number, and any state on that bar (a highlight, an animation in progress, a tooltip) stays with the position, not with the station. A key makes the bar follow the station. This is called object constancy: the same thing on screen means the same thing in the data.
- 2
How to give a key. The second argument of
data()is a function:data(rows, (d) => d.id). D3 calls it for each new item and for each existing element's bound item, and pairs the ones that return the same value. Pairs are update, new keys are enter, old keys with no new match are exit. With the keyed join in the first example, July's Lantern keeps the element that June's Lantern had (el2), Quay keepsel1, Chapel gets a new element, and Library's element is removed. - 3
What the key must be. Something that is unique and stable for the thing: a station
id, a date string, a district name. Not the array index (that is what you had without a key), and not something that changes (a rank, a rides count). Names can be fine if they are unique, but ids are safer because names change. - 4
Keys become text. D3 turns whatever the key function returns into a string before comparing. So
1and"1"are the same key. An object is turned into"[object Object]", so a key function that returns the object itself makes every key equal. An undefined field becomes the text"undefined", which is also the same for every item. The second example shows these three results. - 5
Duplicate keys are not an error, and not a good idea. If the same key appears twice among the elements, D3 pairs the first and sends the repeat to exit. If it appears twice in the new data, the first one is update and the repeat goes to enter. No warning is printed, so the symptom is a chart that has one bar too few or too many, and that quietly recreates elements.
- 6
Order is separate from keys. A keyed join matches elements to items, but the elements in the page must also sit in the data's order.
join()callsorder()for you, which re-inserts elements in the data's order. If you build the join by hand (enter,update,exit), callselection.order()orselection.sort(compare)yourself. For bars placed bytransform, order in the DOM mostly matters for tab order and reading order. - 7
Cost. A keyed join builds a small map of keys on every call. For a few hundred marks this is invisible. A key function that does heavy work (parsing a date for each element) is called once per element and once per item, every update.
Explain it without notes
Why can a join without a key put the wrong value on a bar?
What makes a good key?
What does D3 do when two new items return the same key?
Practice
June's top stations are S01, S06, S04 and July's are S06, S01, S03. Write the keyed join code and say which elements are kept, entered and exited.
Write checkKeys(rows, key) that returns the keys that appear more than once. Test it with ids S01, S02, S01.
Trade-offs
- ↔
A key costs one pass to build a map. It buys correctness whenever data reorders or shrinks from the middle, so use one whenever the items have identity.
Done when you can
I can explain why a position join breaks when data reorders.
I can write
data(rows, (d) => d.id)and say what each group holds.I know keys become strings, and what happens with objects and with missing fields.
I know duplicate keys raise no error and what they do.