When Data Meets the Document

In D3, the difficult question is often not how to draw a mark but how to decide which mark represents which piece of data as the data changes. This chapter follows that decision from preparation through selection, matching, and the enter/update/exit lifecycle. The goal is practical: after reading it, you should be able to predict what a D3 update will do to the document, recognize when identity keys matter, and distinguish binding data from changing the elements that display it.

That distinction gives D3's compositional design a concrete operating contract. Data modules produce values and structures; selections connect those values to document elements; the join determines which elements are new, continuing, or obsolete. The process is incremental rather than an opaque instruction to “redraw the chart.” It is also not automatic interpretation. D3 can carry out the lifecycle, but the author must decide what the data means and what continuity the reader should see.

Preparation changes the question before drawing

A visualization begins with a choice about what counts as a useful value. Real data often arrives as records that are too detailed, too irregular, or too close to its storage format to bind directly to marks. D3 includes tools for fetching and parsing common formats such as CSV and TSV, then operations for grouping, rolling values up, filtering, mapping, reducing, binning, and computing summaries. These mechanisms prepare values for visual use; they do not yet decide how the browser should display them.

Consider a file of daily measurements from several locations. The raw rows answer one question: what was recorded at each place and time? Grouping by location and computing a summary answers another: how do locations compare over a period? Binning the measurements answers a third: how are observations distributed across intervals? All three outputs may be valid, but they support different visual claims. If the application aggregates before binding, the resulting marks represent groups or summaries, not the original observations. A later scale cannot restore the detail that the transformation removed.

This is why preparation belongs as an explicit stage before the join. An opaque chart function can make it difficult to tell whether an apparent pattern came from the source data, an aggregation rule, or a visual encoding. D3's separation lets the author inspect the intermediate values and decide whether they preserve the distinction that matters. It also lets multiple views use the same prepared data while choosing different marks or encodings.

The boundary is not absolute. A data function can be evaluated for each group in a selection, so preparation and binding can be connected when the structure of the document calls for it. But the responsibilities remain distinguishable. Preparation determines the values and relationships that are available. The join determines how those values correspond to elements. Keeping the distinction visible makes a failure easier to locate: an incorrect summary is a data problem, while an element attached to the wrong entity is an identity problem.

D3's data-loading history reinforces the same point. Earlier versions used asynchronous callbacks; later versions adopted Promises and the Fetch API. The important idea for this chapter is not the migration syntax. It is that retrieving data and representing data are separate responsibilities. A completed request supplies input to the preparation stage. It does not by itself create a visual element or define how a later update should preserve continuity.

Binding makes data persistent

A D3 selection is a set of document elements that can be operated on together. When selection.data binds an array—or a data-producing function—to a selection, D3 associates each datum with a corresponding element and stores that bound value on the element as __data__. The datum is therefore “sticky”: later selection operations can recover it, and accessors can use it when setting attributes, text, styles, or geometry.

That persistence is what lets a compact operation describe many marks without manually passing each value through every step. A text accessor can read the datum for each element. A shape accessor can use the same datum to compute coordinates. An event listener can recover the data associated with the element that received the event. The document element becomes the point where data identity and browser state meet.

Binding is not the same as drawing. It does not choose a color, produce a path, or guarantee that an element is visible. It creates a relationship that later operations can use. This distinction matters when a selection already contains elements. Binding may update their associated data, but the author still needs to decide how new elements should be created, how changed elements should be modified, and what should happen to elements for which there is no longer a datum.

The data is specified for each group in a selection. That detail matters when a selection has nested groups, such as rows followed by the cells inside each row. The outer data can provide one row to each group, while the inner data can provide the values that belong to that row. The model is powerful because it mirrors the structure of a document, but it also means that the author must understand which parent and group a data function is serving. A value that is correct for one level can be wrong at another.

The persistence of __data__ also makes identity a visible part of the document lifecycle. If an element is reselected, its datum is still available unless the application deliberately clears it. That makes updates convenient, but it does not make the association semantically correct by itself. D3 can preserve a binding; the author must ensure that the bound value represents the entity the element is meant to show.

Matching is the identity decision

When new data is bound, D3 must match incoming values to existing elements. Without a key function, the default is positional matching: the first datum is matched to the first selected element, the second to the second, and so on. This is reasonable when position itself defines identity—for example, when every slot has a fixed meaning and the data order is stable. It becomes misleading when records can be inserted, removed, or reordered.

Suppose a list of locations is sorted by its latest measurement. Between two updates, one location rises from the bottom to the top. Index matching may keep the existing elements in place while assigning them different locations. The document has changed, but the reader may see each mark as continuing to represent the same entity. A transition can then animate a value from one location into another, creating motion that looks like change within an entity even though the identity has changed.

A key function replaces that positional assumption with an identifier computed for existing elements and incoming data. If the key is the location's stable name, the element representing that location can continue even when its position in the data changes. The update selection follows the data order, while the exit selection preserves the prior selection order until the author removes or reorders elements. The key therefore affects both continuity and document order.

Keys are not magic labels. They encode the author's definition of sameness. A location name may be appropriate when names are unique and stable; it may be unsafe if names can change or collide. A database identifier may preserve entity continuity but fail to distinguish two measurements that should be shown separately. If multiple existing elements have the same key, duplicates enter the exit selection. If multiple incoming data values have the same key, duplicates enter the enter selection. Those outcomes are useful signals that the identity rule and the data structure disagree.

The claim should remain calibrated. D3's documentation says that a key function can minimize changes to the DOM, but the API description does not prove that every keyed update is faster or that every key is semantically correct. A key can preserve the wrong continuity just as easily as it can preserve the right one. The author should ask what the reader is expected to track across updates, then choose an identifier that represents that entity.

sequenceDiagram participant S as New data participant P as Preparation participant J as Selection join participant D as Document elements participant R as Rendered view S->>P: Parse, group, summarize, or reshape P->>J: Provide prepared values J->>J: Match by index or key J->>D: Enter: create missing elements J->>D: Update: retain matched elements J->>D: Exit: remove or transition obsolete elements J->>D: Order: align elements with data order D-->>R: Apply text, attributes, styles, or geometry

The sequence shows why matching belongs in the join rather than inside an unspecified chart. Preparation supplies values, but the selection owns the relationship between values and elements. The join is where that relationship is compared against the existing document, so the application can see whether it has new, continuing, or obsolete entities.

Enter, update, and exit are different outcomes

The three selections produced by a data join represent three distinct states. The enter selection contains data for which no corresponding element exists. Its placeholders point toward the parent, allowing the author to append the missing elements and initialize their attributes or content. The update selection contains elements that successfully matched data. The exit selection contains existing elements for which no new datum was found.

The initial render makes the distinction easy to see. If the document contains no matching elements, every datum enters. The author appends one element for each incoming value, then uses accessors to set its text, geometry, or other properties. A later update may leave some elements in the update selection, create new elements for new values, and place old elements in exit. The document then changes according to the data difference rather than according to a complete replacement of the visual scene.

selection.join provides a concise way to express the common lifecycle. It can append an element for enter, leave update elements as they are, and remove exiting elements by default. It can also accept separate functions so the author controls what happens in each state. Entering marks might receive an initial position, updating marks might change their value, and exiting marks might transition before removal. The enter and update results are merged for subsequent operations, while the exit function's return value is not used as part of that merged selection.

This arrangement is more than a convenience syntax. It makes the lifecycle explicit enough to support different behavior at each transition. A new mark may need initialization because it has no previous geometry. An existing mark may need only a changed attribute. An obsolete mark may need to disappear or be removed after a final state. Treating all three as one undifferentiated redraw would hide those different conditions and make continuity harder to control.

The join also interacts with order. A key function can identify the right elements while leaving the document order different from the data order. D3 provides ordering and sorting operations for that case. This is another boundary between identity and arrangement: two elements can represent the correct entities while appearing in an order that no longer supports the intended reading. Identity answers “which entity is this?” Order answers “where should this entity appear?” A sound update may need both decisions.

There are predictable failure paths. If the data is empty, the update and enter selections contain nothing, and existing elements become candidates for exit. If the key produces duplicates, the lifecycle exposes those duplicates rather than silently deciding which one is authoritative. If an author creates elements without binding the intended data, later accessors cannot recover the relationship they need. If the author binds correctly but never updates the attributes or geometry, the DOM may contain current data while the visible mark remains stale. Each failure occurs at a different point in the contract.

Where the join stops

The join model decides correspondence and lifecycle. It does not decide whether the chosen aggregation answers the question, whether the key represents a real entity, whether a scale communicates magnitude honestly, or whether a visual mark is accessible. It also does not prove that incremental updates are always the best performance strategy. Those decisions remain outside the mechanics of selection.data and selection.join.

That boundary is a strength because it keeps data identity from being confused with visual meaning. It is also a responsibility. The author must carry a coherent definition of the entity from preparation into binding, from binding into encoding, and from encoding into interaction. When that chain is sound, D3 can preserve continuity while the graphic changes. When it is not, a smooth update can make an incorrect relationship look persuasive.

The essential result is simple: D3 does not merely attach numbers to shapes. It gives the author a way to state which values exist, which elements represent them, and how those representations enter, continue, or leave the document. With that data-to-document contract in place, the next judgment is how the resulting values should be mapped into visual meaning without confusing a technical mechanism with an interpretation.

References