Topic 5.6
Events in D3
In one line
selection.on("click", (event, d) => ...) attaches a listener to every selected mark, and gives it the browser's event and the mark's datum. Learn the version 7 form, how to remove listeners, why re-running draw must not stack them, and when one listener on the parent beats one per mark.
Think of it like this
A restaurant with 25 tables, each with a call bell. Fitting a bell to each table works, but whenever a table is added someone must fit a new bell. A single waiter at the door who watches for any raised hand and notes which table it came from does the same job with one person. The first is a listener per mark; the second is event delegation.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- event
- A message from the browser that something happened: a click, a key press, a pointer move.
- listener
- A function you attach to an element that runs when a given event reaches it.
- event object
- The details of one event: its type, the element it hit (
target), the pointer position. - namespace
- A label after a dot, such as
click.log, that lets D3 keep several listeners for one event on one element. - event delegation
- Listening on a parent for events that come from its children, and checking which child it was.
- bubbling
- An event travelling from the element it hit up through its parents, so a parent's listener can hear it.
Step by step
01Attach a click to every bar
The first example makes three bars with a click listener that gets (event, d). The script clicks Tuesday's bar with dispatchEvent. The Console shows the datum (Tue), the event's type, that event.target === this and the tag of this, rect.
02Change the datum, click again, then remove the listener
The example sets a new datum on Tuesday's bar, and the next click reports Tue*, the new value. Then on("click", null) removes the listeners: a click on Monday's bar changes nothing, and on("click") returns undefined.
03Count the listeners
In the second example draw runs three times and adds two kinds of listener. One click then ran the D3 one once and the raw addEventListener one three times. Delegation: one listener on the svg served all 40 bars, ignored a click on the background, and two namespaced listeners (click.a, click.b) both ran until click.a was removed.
04Read the pointer position
The third example dispatches a pointermove and reads d3.pointer(event): the position inside the SVG, in the SVG's own units.
Try it yourself
- 1
Click Wednesday
In the first example change
click(bars[1])(the first call) toclick(bars[2]). The Console printsclick on Wedand the message becomesWed: 3759 rides. - 2
Use a namespace to keep both
In the first example add
.on("click.second", () => console.log("second"))after the first.on(...)call. One click now prints both the first listener's line andsecond, because the names differ. Write.on("click", () => console.log("second"))instead and the first listener is replaced.
Code & diagrams
Preview
Console
Nothing logged yet.Expected console output
click on Tue | event.type: click | target is this: true | this: rect
message: Tue: 3992 rides
click on Tue* | event.type: click | target is this: true | this: rect
after on('click', null) the message is still: Tue*: 1 rides
listener getter: undefinedPreview
Console
Nothing logged yet.Expected console output
after 3 draws and one click: d3 handler ran 1 time, raw handler ran 3 times
delegated click on S13
delegated handler ran 1 time (the background click was ignored)
a
b
b`d3.pointer` subtracts the SVG's position on the page, so the numbers are the same wherever the chart sits.
Preview
Console
Nothing logged yet.Expected console output
pointer at 100 50
pointer at 5 115Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
The old d3.event
In the first example replace the handler with .on("click", function (d) { console.log(d3.event.target.tagName); }), the version 5 form.
Myth vs fact
Myth
I have to re-attach listeners after every update.
Fact
Not for elements that stay: the listener stays and reads the element's current datum. You only attach to elements the join creates.
Myth
on and addEventListener do the same thing.
Fact
on replaces a listener of the same name on that element. addEventListener always adds another, which is how handlers run three times after three updates.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
D3 stores listeners by type and name in a property on the element (
__on) and registers one real browser listener per type and name. Replacing with the same name removes the old one first. That is whyonis idempotent. - ▸
on("click", fn, { passive: true })passes options toaddEventListener. Scroll-blocking events such astouchmoveandwheellisteners should be passive, or the page's scrolling waits for your code (Topic 8.5).
Remember this
- 1
The problem this solves: a chart that cannot be clicked or pointed at is a picture. Events connect what the learner does (a click, a pointer moving across a bar) to what the code knows about that mark (its datum). D3 hands you both in one function.
- 2
The version 7 form.
selection.on("click", (event, d) => { ... }). The first argument is the browser's real event object (event.type,event.target,event.clientX). The second is the datum bound to the element that received it. Inside afunction () {},thisis that element. Old code (version 5 and earlier) wrotefunction (d) { d3.event... }:d3.eventis gone, and reading it throwsTypeError: Cannot read properties of undefined. The breakIt shows the error text. - 3
The datum is read when the event happens. If you update the data between the draw and the click, the listener sees the new datum, because D3 reads the element's current
__data__each time. You do not need to re-attach listeners after an update, only after a new element is created. - 4
One name, one listener.
on("click", fn)replaces an earlierclicklistener that D3 attached to the same element. So calling it again on everydrawis safe. Add a namespace ("click.highlight","click.log") to keep several independent listeners for the same event. Remove one withon("click", null)oron("click.log", null). Readingon("click")with no listener returnsundefined. - 5
Double-binding.
element.addEventListener("click", fn)is different: it adds another listener every time. If adrawfunction calls it for every bar on every update, then after three updates one click runs the handler three times. The second example showsd3.ongiving 1 andaddEventListenergiving 3. Useon, or attach once when the element is created. - 6
Pointer events and coordinates. Use
pointerenter,pointermove,pointerleaveandpointerdowninstead ofmouse...: one set of events covers a mouse, a pen and a finger (Topic 8.1 goes deeper).d3.pointer(event)returns[x, y]in the coordinates of the event'scurrentTargetelement, such as the SVG, so it is already in the same units as your drawing. - 7
Delegation. For many marks (hundreds), one listener on the parent is lighter than one per mark:
svg.on("click", (event) => { const bar = event.target.closest("rect"); if (!bar) return; ... d3.select(bar).datum() }). A click on the empty background has norectancestor, so the handler returns early. Delegation also means new bars need no new listener. - 8
Failure cases. A click on a text label inside a
ghasevent.targetset to thetext, not therect:closestfinds the right ancestor. A handler that throws is reported as an uncaught error in the console and the page keeps running. A thin line is hard to hit: give it an invisible wide partner (Topic 8.1). A touch screen has no hover, so never hide information behindpointerenteralone.
Explain it without notes
What arguments does a D3 7 listener receive?
Why does a listener added with addEventListener run three times after three draw calls, but one added with on run once?
When is event delegation better than a listener per bar?
Practice
Write a draw(rows) that makes bars and a click listener that writes the station name into #name. Make sure calling it three times leaves one listener per bar.
Rewrite it with delegation: one listener on the svg. What changes when a click lands on the background?
Trade-offs
- ↔
A listener per mark is simple to read and costs memory per element. Delegation is lighter and needs
closestlogic.
Done when you can
I can attach a click with
(event, d)and read the datum.I know why
d3.eventfails in D3 7.I can remove a listener and use namespaces for several.
I can explain double-binding and how
onavoids it.