Chapter 4. Events, Interactivity, and Animation
When rendered by a browser, SVG elements can receive user events and can be manipulated as a whole (for example, to change their position or appearance). This means that they behave essentially like widgets in a GUI toolkit. Thatâs an exciting proposition: to regard SVG as a widget set for graphics. This chapter discusses the options available to create those essential features of a user interface: interactivity and animation.
Events
An important aspect of the DOM is its event model: essentially any DOM element can receive events and invoke an appropriate handler. The number of different event types is very large; most important for our purposes are user-generated events (mouse clicks or movements, as well as keystrokes; see Table 4-1).1
Function | Description |
---|---|
|
Any mouse button is pressed and released on an element. |
|
The mouse is moved while over an element. |
|
A mouse button is pressed or released over an element. |
|
The mouse pointer is moved onto or off of an element. |
|
The mouse pointer is moved onto or off of an element, or any of its children. |
|
Any key is pressed or released. |
D3 treats event handling as part of the Selection
abstraction (see
Table 4-2). If sel
is a Selection
instance, then you use
the following member function to register a callback as event handler
for the specified event type:
sel
.
on
(
type
,
callback
)
The type
argument must be a string indicating the event type (such as
"click"
). Any DOM event type is permitted.
If a handler was already registered for the event type via on()
, it is
removed before the new handler is registered. To explicitly remove the
handler for a given event type, provide null
as the second argument.
To register multiple handlers for the same event type, the type
name may be followed by a period and an arbitrary tag (so that
the handler for "click.foo"
does not overwrite the one for "click.bar"
).
The callback is a function invoked when an event of the
specified type is received by any element of the selection. The
callback will be invoked in the same way as any other accessor is
invoked in the context of a selection,
being passed the data point d
bound to the current element, the elementâs
index i
in the current selection, and the nodes in the current
selection, while this
contains the current element itself.2 The actual event instance
is not passed to the callback as an argument, but it is available in
the variable:
d3
.
event
When an event occurs, this variable contains the raw DOM event instance (not a D3 wrapper!). The information provided by the event object itself depends on the event type. For mouse events, naturally the location of the mouse pointer when the event occurred is of particular interest. The event object contains the mouse coordinates with respect to three different coordinate systems,3 but none directly provides the information that would be most useful, namely the position with respect to the containing parent element! Thankfully, they can be obtained using:
(((
"d3.mouse() function"
)))
d3
.
mouse
(
node
)
This function returns the mouse coordinates as a two-element array [x, y]
.
The argument should be the enclosing container element (as a DOM Node
,
not as Selection
). When working with SVG, you can supply any element
(as a Node
), and the function will calculate the coordinates relative to
the nearest ancestor SVG element.
Function | Description |
---|---|
|
Adds or removes a callback for each element in the selection. The
|
|
Contains the current event, if any, as a DOM |
|
Returns a two-element array containing the mouse coordinates relative to the specified parent. |
|
Exploring Graphs with the Mouse
For someone working analytically with data, these features offer some exciting opportunities because they make it easy to explore
graphs interactively: point the mouse at a spot on the graph and get additional information about the data point located there. Here is a simple example. If you call the function in Example 4-1,
while supplying a CSS selector string (see âCSS Selectorsâ) that
identifies an <svg>
element, the current mouse pointer location (in
pixel coordinates) will be shown in the graph itself. Moreover, the
location of the textual display is not fixed but will move together with the mouse pointer.
Example 4-1. Given a CSS selector string, this function will continuously display the mouse position in pixel coordinates whenever the user moves the mouse.
function
coordsPixels
(
selector
)
{
var
txt
=
d3
.
select
(
selector
)
.
append
(
"text"
)
;
var
svg
=
d3
.
select
(
selector
)
.
attr
(
"cursor"
,
"crosshair"
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
txt
.
attr
(
"x"
,
18
+
pt
[
0
]
)
.
attr
(
"y"
,
6
+
pt
[
1
]
)
.
text
(
""
+
pt
[
0
]
+
","
+
pt
[
1
]
)
;
}
)
;
}
Create the
<text>
element to display the coordinates. It is important to do this outside of the event callback; otherwise, a new<text>
element will be created every time the user moves the mouse!Change the shape of the mouse cursor while over the
<svg>
element. This is not required, of courseâbut it is a fitting effect (and also demonstrates how the mouse cursor can be changed through attributes; see Appendix B).Obtain the mouse coordinates, relative to the upper-left corner of the
<svg>
element, using thed3.mouse()
convenience function.Update the text element created earlier. In this example, both the displayed text content of the element and its position are updated: slightly to the right of the mouse position.
Displaying the mouse coordinates is, of course, neither new nor particularly exciting. But what is exciting is to see just how easy it is to implement such behavior in D3!
Case Study: Simultaneous Highlighting
The next example is more interesting. It addresses a common problem when working with multivariate data sets: how to link two different views or projections of the data visually. One way is to select a region of data points in one view with the mouse and simultaneously highlight the corresponding points in all other views. In Figure 4-1, points are highlighted in both panels according to their distance (in pixel coordinates) from the mouse pointer in the lefthand panel. Because this example is more involved, we will first discuss a simplified version (see Example 4-2).
Example 4-2. Commands for Figure 4-1
function
makeBrush
(
)
{
d3
.
csv
(
"dense.csv"
)
.
then
(
function
(
data
)
{
var
svg1
=
d3
.
select
(
"#brush1"
)
;
var
svg2
=
d3
.
select
(
"#brush2"
)
;
var
sc1
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
10
,
50
]
)
.
range
(
[
"lime"
,
"yellow"
,
"red"
]
)
;
var
sc2
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
10
,
50
]
)
.
range
(
[
"lime"
,
"yellow"
,
"blue"
]
)
;
var
cs1
=
drawCircles
(
svg1
,
data
,
d
=>
d
[
"A"
]
,
d
=>
d
[
"B"
]
,
sc1
)
;
var
cs2
=
drawCircles
(
svg2
,
data
,
d
=>
d
[
"A"
]
,
d
=>
d
[
"C"
]
,
sc2
)
;
svg1
.
call
(
installHandlers
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
;
}
)
;
}
function
drawCircles
(
svg
,
data
,
accX
,
accY
,
sc
)
{
var
color
=
sc
(
Infinity
)
;
return
svg
.
selectAll
(
"circle"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"circle"
)
.
attr
(
"r"
,
5
)
.
attr
(
"cx"
,
accX
)
.
attr
(
"cy"
,
accY
)
.
attr
(
"fill"
,
color
)
.
attr
(
"fill-opacity"
,
0.4
)
;
}
function
installHandlers
(
svg
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
{
svg
.
attr
(
"cursor"
,
"crosshair"
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
cs1
.
attr
(
"fill"
,
function
(
d
,
i
)
{
var
dx
=
pt
[
0
]
-
d3
.
select
(
this
)
.
attr
(
"cx"
)
;
var
dy
=
pt
[
1
]
-
d3
.
select
(
this
)
.
attr
(
"cy"
)
;
var
r
=
Math
.
hypot
(
dx
,
dy
)
;
data
[
i
]
[
"r"
]
=
r
;
return
sc1
(
r
)
;
}
)
;
cs2
.
attr
(
"fill"
,
(
d
,
i
)
=>
sc2
(
data
[
i
]
[
"r"
]
)
)
;
}
)
.
on
(
"mouseleave"
,
function
(
)
{
cs1
.
attr
(
"fill"
,
sc1
(
Infinity
)
)
;
cs2
.
attr
(
"fill"
,
sc2
(
Infinity
)
)
;
}
)
;
}
Load the data set and specify the callback to invoke when the data is available (see Chapter 6 for more information about fetching data). The file contains three columns, labeled
A
,B
, andC
.Select the two panels of the graph.
D3 can smoothly interpolate between colors. Here we create two color gradients (one for each panel). (See Chapter 7 to learn more about interpolation and scale objects.)
Create the circles representing data points. The newly created circles are returned as
Selection
objects. Following a general D3 convention, columns are specified in the function call by providing accessor functions.Call the function
installHandlers()
to register the event handlers. This line of code uses thecall()
facility to invoke theinstallHandlers()
function, while supplying thesvg1
selection and the remaining parameters as arguments. (We encountered this already in Example 2-6; also see the discussion regarding components in Chapter 5.)Initially, the circles are drawn with the âmaximumâ color. To find this color, evaluate the color scale at positive infinity.
For each point in the panel on the left, calculate its distance to the mouse pointerâ¦
⦠and store it, as an additional column, in the data set. (This will be our mechanism of communication between the two panels of the figure.)
Return the appropriate color from the color gradient.
Use the additional column in the data set to color the points in the panel on the right.
Restore the points to their original colors when the mouse leaves the lefthand panel.
This version of the program works well and solves the original problem. The improved version of the installHandlers()
function shown in
Example 4-3 allows us to discuss some additional techniques
when writing this kind of user interface code.
Example 4-3. An improved version of the installHandlers() function in Example 4-2
function
installHandlers2
(
svg
,
data
,
cs1
,
cs2
,
sc1
,
sc2
)
{
var
cursor
=
svg
.
append
(
"circle"
)
.
attr
(
"r"
,
50
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"black"
)
.
attr
(
"stroke-width"
,
10
)
.
attr
(
"stroke-opacity"
,
0.1
)
.
attr
(
"visibility"
,
"hidden"
)
;
var
hotzone
=
svg
.
append
(
"rect"
)
.
attr
(
"cursor"
,
"none"
)
.
attr
(
"x"
,
50
)
.
attr
(
"y"
,
50
)
.
attr
(
"width"
,
200
)
.
attr
(
"height"
,
200
)
.
attr
(
"visibility"
,
"hidden"
)
.
attr
(
"pointer-events"
,
"all"
)
.
on
(
"mouseenter"
,
function
(
)
{
cursor
.
attr
(
"visibility"
,
"visible"
)
;
}
)
.
on
(
"mousemove"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
svg
.
node
(
)
)
;
cursor
.
attr
(
"cx"
,
pt
[
0
]
)
.
attr
(
"cy"
,
pt
[
1
]
)
;
cs1
.
attr
(
"fill"
,
function
(
d
,
i
)
{
var
dx
=
pt
[
0
]
-
d3
.
select
(
this
)
.
attr
(
"cx"
)
;
var
dy
=
pt
[
1
]
-
d3
.
select
(
this
)
.
attr
(
"cy"
)
;
var
r
=
Math
.
hypot
(
dx
,
dy
)
;
data
[
i
]
[
"r"
]
=
r
;
return
sc1
(
r
)
;
}
)
;
cs2
.
attr
(
"fill"
,
(
d
,
i
)
=>
sc2
(
data
[
i
]
[
"r"
]
)
)
;
}
)
.
on
(
"mouseleave"
,
function
(
)
{
cursor
.
attr
(
"visibility"
,
"hidden"
)
;
cs1
.
attr
(
"fill"
,
sc1
(
Infinity
)
)
;
cs2
.
attr
(
"fill"
,
sc2
(
Infinity
)
)
;
}
)
}
In this version, the actual mouse pointer itself is hidden and replaced with a large, partially opaque circle. Points within the circle will be highlighted.
Initially, the circle is hidden. It will only be shown once the mouse pointer enters the âhot zone.â
The âhot zoneâ is defined as a rectangle inside the lefthand panel. The event handlers are registered on this rectangle, meaning that they will only be invoked when the mouse pointer is inside of it.
The rectangle is hidden from view. By default, DOM elements that have their
visibility
attribute set tohidden
do not receive mouse pointer events. To overcome this, thepointer-events
attribute must be set explicitly. (Another way to make an element invisible is to set itsfill-opacity
to0
. In this case, it will not be necessary to modify thepointer-events
attribute.)When the mouse enters the âhot zone,â the opaque circle that acts as the pointer is displayed.
The
mousemove
andmouseleave
event handlers are equivalent to the ones in Example 4-2, except for the additional commands to update the circle acting as a cursor.
The use of an active âhot zoneâ in this example is of course optional,
but it demonstrates an interesting technique. At the same time, the
discussion of the pointer-events
attribute suggests that this kind
of user interface programming may involve unexpected challenges. We
will come back to this point after the next example.
The D3 Drag-and-Drop Behavior Component
Several common user interface patterns consist of a combination of events and responses: in the drag-and-drop pattern, for instance, the user first selects an item, then moves it, and finally releases it again. D3 includes a number of predefined behavior components that simplify the development of such user interface code by bundling and organizing the required actions. In addition, these components also unify some details of the user interface.
Consider a situation like the one in Figure 4-2, showing the following SVG snippet:
<svg
id=
"dragdrop"
width=
"600"
height=
"200"
>
<circle
cx=
"100"
cy=
"100"
r=
"20"
fill=
"red"
/>
<circle
cx=
"300"
cy=
"100"
r=
"20"
fill=
"green"
/>
<circle
cx=
"500"
cy=
"100"
r=
"20"
fill=
"blue"
/>
</svg>
Now letâs enable the user to change the position of the circles with
the mouse.
It is not difficult to add the familiar drag-and-drop pattern by
registering callbacks for mousedown
, mousemove
, and mouseup
events, but Example 4-4 uses the D3 drag
behavior
component instead.
As explained in Example 2-6, a component is a function object
that takes a Selection
instance as argument and adds DOM elements to
that Selection
(also see Chapter 5). A behavior component is a
component that installs required event callbacks in the DOM tree.
At the same time, it is also an object that has member functions itself.
The listing uses the drag componentâs on( type, callback )
member
function to specify the callbacks for the different event types.
Example 4-4. Using the drag-and-drop behavior
function
makeDragDrop
(
)
{
var
widget
=
undefined
,
color
=
undefined
;
var
drag
=
d3
.
drag
(
)
.
on
(
"start"
,
function
(
)
{
color
=
d3
.
select
(
this
)
.
attr
(
"fill"
)
;
widget
=
d3
.
select
(
this
)
.
attr
(
"fill"
,
"lime"
)
;
}
)
.
on
(
"drag"
,
function
(
)
{
var
pt
=
d3
.
mouse
(
d3
.
select
(
this
)
.
node
(
)
)
;
widget
.
attr
(
"cx"
,
pt
[
0
]
)
.
attr
(
"cy"
,
pt
[
1
]
)
;
}
)
.
on
(
"end"
,
function
(
)
{
widget
.
attr
(
"fill"
,
color
)
;
widget
=
undefined
;
}
)
;
drag
(
d3
.
select
(
"#dragdrop"
)
.
selectAll
(
"circle"
)
)
;
}
Create a
drag
function object using the factory functiond3.drag()
, then invoke theon()
member function on the returned function object to register the required callbacks.The
start
handler stores the current color of the selected circle; then changes the selected circleâs color and assigns the selected circle itself (as aSelection
) towidget
.The
drag
handler retrieves the current mouse coordinates and moves the selected circle to this location.The
end
handler restores the circleâs color and clears the activewidget
.Finally, invoke the
drag
component operation while supplying a selection containing the circles to install the configured event handlers on the selection.
A more idiomatic way to express this would use the call()
function rather than invoking the component operation explicitly:
d3
.
select
(
"#dragdrop"
).
selectAll
(
"circle"
)
.
call
(
d3
.
drag
()
.
on
(
"start"
,
function
()
{
...
}
)
.
on
(
"drag"
,
function
()
{
...
}
)
.
on
(
"end"
,
function
()
{
...
}
)
);
The event names in Example 4-4 may come as a surprise: these
are not standard DOM events, but D3 pseudoevents.
The D3 drag
behavior combines both mouse and touch-screen event handling.
Internally, the start
pseudoevent corresponds to either a mousedown
or a touchstart
event, and similar for drag
and end
. Furthermore,
the drag
behavior prevents the browserâs default action for certain
event types.4
D3 includes additional behaviors to assist with zooming and when
selecting parts of a graph with a mouse.
Notes on User Interface Programming
I hope the examples so far have convinced you that creating interactive graphs using D3 need not be difficultâin fact, I believe D3 makes them feasible even for ad hoc, one-off tasks and explorations. At the same time, as the discussion after the previous two examples shows, graphical user interface programming is still a relatively complex problem. Many components, each with its own rules, participate and can interact in unexpected ways. Browsers may differ in their implementation. Here are some reminders and potential surprises (also see Appendix C for background information on DOM event handling):
-
Repeated calls to
on()
for the same event type on the sameSelection
instance clobber each other. Add a unique tag to the event type (separated by a period) to register multiple event handlers. -
If you want to access
this
in a callback or accessor function, you must use thefunction
keyword, you cannot use an arrow function. This is a limitation of the JavaScript language (see Appendix C). Examples can be found in theinstallHandlers()
function in Examples 4-2 and 4-3, and several times in Example 4-4. -
Browser default behavior may interfere with your code; you may need to prevent it explicitly.
-
Generally, only visible, painted elements can receive mouse pointer events. Elements with their
visibility
attribute set tohidden
, or with bothfill
andstroke
set tonone
, do not receive pointer events by default. Use thepointer-events
attribute for fine-grained control over the conditions under which elements will receive events. (See MDN Pointer-Events.) -
In a similar spirit, a
<g>
element has no visual representation, and hence does not generate pointer events. Nevertheless, it may be appropriate to register an event handler on a<g>
element because events generated by any of its (visible) children will be delegated to it. (Use an invisible rectangle or other shape to define active âhot zones,â as in Example 4-3.)
Smooth Transitions
An obvious way to respond to events is to apply some change to the figureâs appearance or configuration (for example, to show a before-and-after effect). In this case, it is often useful to let the change take place gradually, rather than instantaneously, to draw attention to the change that is taking place and to allow users to discern additional detail. For example, users may now be able to recognize which data points are most affected by the change, and how (see Figure 3-3 and Example 3-1 for an example).
Conveniently, the D3 Transition
facility does all the work for you.
It replicates most of the Selection
API, and you can change the
appearance of selected elements using attr()
or style()
as before
(see Chapter 3). But now the new settings do not take effect
immediately; instead, they are applied gradually over a configurable
time span (see Example 2-8 for an early example).
Under the covers, D3 creates and schedules the required intermediate configurations to give the appearance that the graph is changing smoothly over the desired duration. To do so, D3 invokes an interpolator that creates the intermediate configurations between the starting and end points. The D3 interpolation facility is fairly smart and able to interpolate automatically between most types (such as numbers, dates, colors, strings with embedded numbers, and moreâsee Chapter 7 for a detailed description).
Creating and Configuring Transitions
The workflow to create a transition is simple (also see Table 4-3):
-
Before creating a transition, make sure any data has been bound and all elements that are supposed to be part of the transition have been created (using
append()
orinsert()
)âeven if they are initially set to be invisible! (TheTransition
API allows you to change and remove elements, but it does not provide for the creation of elements as part of the transition.) -
Now select the elements you wish to change using the familiar
Selection
API. -
Invoke
transition()
on this selection to create a transition. Optionally, callduration()
,delay()
, orease()
for more control over its behavior. -
Set the desired end state using
attr()
orstyle()
as usual. D3 will create the intermediate configurations between the current values and the indicated end states, and apply them over the duration of the transition.
Often, these commands will be part of an event handler, so that the transition starts when an appropriate event occurs.
Function | Description |
---|---|
|
Returns a new transition on the receiving selection. The optional argument
may be a string (to identify and distinguish this transition on the selection)
or a |
|
Stops the active transition and cancels any pending transitions on the selected elements for the given identifier. (Interrupts are not forwarded to children of the selected elements.) |
|
Returns a new transition on the same selected elements as the receiving transition, scheduled to start when the current transition ends. The new transition inherits the current transitionâs configuration. |
|
Returns the selection for a transition. |
In addition to the desired end point, a Transition
also allows you to
configure several aspects of its behavior (see Table 4-4).
All of these have reasonable defaults, making explicit configuration optional:
-
A delay that must pass before the change begins to take effect.
-
An easing that controls how the rate of change will differ over the transition duration (to âease intoâ and âout ofâ the animation). By default, the easing follows a piecewise cubic polynomial with âslow-in, slow-outâ behavior.
-
An interpolator to calculate the intermediate values (this is rarely necessary, because the default interpolators handle most common configurations automatically).
-
An event handler to execute custom code when the transition starts, ends, or is interrupted.
Function | Description |
---|---|
|
Sets the delay (in milliseconds) before the transition begins for each
element in the selection; the default is 0. The delay
can be given as a constant or as a function. If it is a function, the
function will be invoked once for each element, before the transition
begins, and should return the desired delay. The function will be passed
the data bound to the element |
|
Sets the duration (in milliseconds) of the transition for each
element in the selection; the default is 250 milliseconds. The duration
can be given as a constant or as a function. If it is a function, the
function will be invoked once for each element, before the transition
begins, and should return the desired duration. The function will be passed
the data bound to the element |
|
Sets the easing function for all selected elements. The easing must be
a function, taking a single parameter between 0 and 1, and returning a
single value, also between 0 and 1. The default easing is |
|
Adds an event handler on the transition. The type must be |
Using Transitions
The Transition
API replicates large parts of the Selection
API.
In particular, all functions from Table 3-2 (that is,
select()
, selectAll()
, and filter()
) are available. From
Table 3-4, attr()
, style()
, text()
, and each()
carry over, as well as all functions from Table 3-5 except
append()
, insert()
, and sort()
. (As was pointed out earlier, all
elements participating in a transition must exist before the transition
is created. For the same reason, none of the functions for binding data
from Table 3-3 exist for transitions.)
Basic transitions are straightforward to use, as we have already seen in
an example in an earlier chapter (Example 3-1). The application
in Example 4-5 is still simple, but the effect is more
sophisticated: a bar chart is updated with new data, but the effect is
staggered (using delay()
) so that the bars donât all change at the
same time.
Example 4-5. Using transitions (see Figure 4-3)
function
makeStagger
(
)
{
var
ds1
=
[
2
,
1
,
3
,
5
,
7
,
8
,
9
,
9
,
9
,
8
,
7
,
5
,
3
,
1
,
2
]
;
var
ds2
=
[
8
,
9
,
8
,
7
,
5
,
3
,
2
,
1
,
2
,
3
,
5
,
7
,
8
,
9
,
8
]
;
var
n
=
ds1
.
length
,
mx
=
d3
.
max
(
d3
.
merge
(
[
ds1
,
ds2
]
)
)
;
var
svg
=
d3
.
select
(
"#stagger"
)
;
var
scX
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
n
]
)
.
range
(
[
50
,
540
]
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
mx
]
)
.
range
(
[
250
,
50
]
)
;
svg
.
selectAll
(
"line"
)
.
data
(
ds1
)
.
enter
(
)
.
append
(
"line"
)
.
attr
(
"stroke"
,
"red"
)
.
attr
(
"stroke-width"
,
20
)
.
attr
(
"x1"
,
(
d
,
i
)
=>
scX
(
i
)
)
.
attr
(
"y1"
,
scY
(
0
)
)
.
attr
(
"x2"
,
(
d
,
i
)
=>
scX
(
i
)
)
.
attr
(
"y2"
,
d
=>
scY
(
d
)
)
;
svg
.
on
(
"click"
,
function
(
)
{
[
ds1
,
ds2
]
=
[
ds2
,
ds1
]
;
svg
.
selectAll
(
"line"
)
.
data
(
ds1
)
.
transition
(
)
.
duration
(
1000
)
.
delay
(
(
d
,
i
)
=>
200
*
i
)
.
attr
(
"y2"
,
d
=>
scY
(
d
)
)
;
}
)
;
}
Define two data sets. To keep things simple, only the y values are included; we will be using the array index of each item for its horizontal position.
Find the number of data points, and the overall maximal value across both data sets.
Two scale objects that map the values in the data set to vertical, and their index positions in the array to horizontal pixel coordinates.
Create the bar chart. Each âbarâ is realized as a thick line (rather than a
<rect>
element).Register an event handler for
"click"
events.Interchange the data sets.
Bind the (updated) data set
ds1
to the selectionâ¦â¦ and create a transition instance. Each bar will take one second to attain its new size, but will start only after a delay. The delay is dependent on the horizontal position of each bar, growing left to right. This has the effect that the âupdateâ seems to sweep across the chart.
Finally, set the new vertical length of each line. This is the end point for the transition.
Hints and Techniques
Not all transitions are as straightforward as the ones we have seen so far. Here are some additional hints and techniques.
Strings
The D3 default interpolators will interpolate numbers that are embedded in
strings, but leave the rest of the string alone because there is no
generally useful way to interpolate between strings. The best way
to achieve a smooth transition between strings is to cross-fade between
two strings in the same location. Assume that two suitable <text>
elements exist:
<text
id=
"t1"
x=
"100"
y=
"100"
fill-opacity=
"1"
>
Hello</text>
<text
id=
"t2"
x=
"100"
y=
"100"
fill-opacity=
"0"
>
World</text>
Then you can cross-fade between them by changing their opacity (possibly changing the duration of the transition):
d3
.
select
(
"#t1"
).
transition
().
attr
(
"fill-opacity"
,
0
);
d3
.
select
(
"#t2"
).
transition
().
attr
(
"fill-opacity"
,
1
);
An alternative that may make sense in certain cases is to write a custom interpolator to generate intermediate string values.
Chained transitions
Transitions can be chained so that one transition begins when the first one ends. The subsequent transitions inherit the earlier transitionâs duration and delay (unless they are overridden explicitly). The following code will turn the selected elements first to red, then to blue:
d3
.
selectAll
(
"circle"
)
.
transition
().
duration
(
2000
).
attr
(
"fill"
,
"red"
)
.
transition
().
attr
(
"fill"
,
"blue"
);
Explicit starting configuration
Unless you plan to use a custom interpolator (see next), it is important
that the starting configuration is set explicitly. For example, donât rely
on the default value (black
) for the fill
attribute: unless the fill
attribute is set explicitly, the default interpolator will not know what to do.
Custom interpolators
Using the methods in Table 4-5, it is possible to
specify a custom interpolator to be used during the transition. The
methods to set a custom interpolator take a factory function as argument.
When the transition starts, the factory function is invoked for each
element in the selection, being passed the data d
bound to the element
and the elementâs index i
, with this
being set to the current DOM
Node
. The factory must return an interpolator function. The
interpolator function must accept a single numeric argument between 0
and 1 and must return an appropriate intermediate value between the starting
and the end configuration. The interpolator will be called after any easing
has been applied.
The following code uses a simple custom color interpolator without
easing (see Chapter 8 to learn about more flexible ways to operate
on colors in D3):
d3
.
select
(
"#custom"
).
selectAll
(
"circle"
)
.
attr
(
"fill"
,
"white"
)
.
transition
().
duration
(
2000
).
ease
(
t
=>
t
)
.
attrTween
(
"fill"
,
function
()
{
return
t
=>
"hsl("
+
360
*
t
+
", 100%, 50%)"
}
);
The next example is more interesting. It creates a rectangle centered at the position (100, 100) in the graph and then rotates the rectangle smoothly around its center. (D3 default interpolators understand some SVG transformations, but this example demonstrates how to write your own interpolator in case you need to.)
d3
.
select
(
"#custom"
).
append
(
"rect"
)
.
attr
(
"x"
,
80
).
attr
(
"y"
,
80
)
.
attr
(
"width"
,
40
).
attr
(
"height"
,
40
)
.
transition
().
duration
(
2000
).
ease
(
t
=>
t
)
.
attrTween
(
"transform"
,
function
()
{
return
t
=>
"rotate("
+
360
*
t
+
",100,100)"
}
);
Transition events
Transitions emit custom events when they start, end, and are interrupted.
Using the on()
method, you can register an event handler on a transition,
which will be called when the appropriate lifecycle event is emitted.
(See the D3 Reference Documentation for details.)
Easings
Using the ease()
method, you can specify an easing. The purpose of an easing is to âstretchâ or âcompressâ the time seen by the interpolator, allowing the animation to âease into and out ofâ the movement. This often
enhances the visual affect of an animation dramatically. As a matter of
fact, âslow-in, slow-outâ has been recognized by animators at Disney as
one of the âTwelve Principles of Animationâ (see âPrinciples of Animationâ).
But at other times, when they donât match well with the way the user expects an object to behave, easings can be downright confusing. The balance is definitely subtle.
An easing takes a parameter in the interval [0, 1] and maps it to the
same interval, starting for t = 0 at 0 and ending for t = 1 at 1.
The mapping is typically nonlinear (otherwise, the easing is just
the identity). The default easing is d3.easeCubic
, which implements
a version of âslow-in, slow-outâ behavior.
Technically, an easing is simply a mapping that is applied to the time parameter t before it is passed to the interpolator. This makes the distinction between the easing and the interpolator somewhat arbitrary. What if a custom interpolator itself mangles the time parameter in some nonlinear way? From a practical point of view, it seems best to treat easings as a convenience feature that adds âslow-in, slow-outâ behavior to standard interpolators. (D3 includes a confusingly large range of different easings, some of which considerably blur the distinction between an easing and what should be considered a custom interpolator.)
Donât overuse transitions
Transitions can be overused. A general problem with transitions is that they usually canât be interrupted by the user: the resulting forced wait can quickly lead to frustration. When transitions are employed to allow the user to track the effects of a change, they aid understanding (see Figure 3-3 for a simple example). But when they are used just âfor effect,â they easily become tiring once the initial cuteness wears off. (Figure 4-3 can serve as a cautionary example in this spirit!)
Animation with Timer Events
Transitions are a convenience technique to transform a configuration smoothly into another, but they are not intended as framework for general animations. To create those, it is generally necessary to work on a lower level. D3 includes a special timer that will invoke a given callback once per animation frame, that is, every time the browser is about to repaint the screen. The time interval is not configurable because it is determined by the browserâs refresh rate (about 60 times per second or every 17 milliseconds, for most browsers). It is also not exact; the callback will be passed a high-precision timestamp that can be used to determine how much time has passed since the last invocation (see Table 4-6).
Function | Description |
---|---|
|
Returns a new timer instance. The timer will invoke the callback
perpetually once per animation frame. When invoked, the callback will be
passed the apparent elapsed time since the timer started running.
(Apparent elapsed time does not progress while the window
or tab is in the background.) The numeric |
|
Like |
|
Similar to |
|
Stops this timer. Has no effect if the timer is already stopped. |
|
Returns the current time, in milliseconds. |
Example: Real-Time Animations
Example 4-6 creates a smooth animation by updating a graph
for every browser repaint. The graph (see the left side of
Figure 4-4) draws a line (a Lissajous
curve5)
that slowly fades as time goes on.
In contrast to most other examples, this code does not use bindingâmostly
because there is no data set to bind! Instead, at each time step, the next
position of the curve is calculated and a new <line>
element is added to the graph from the previous position to the new one. The opacity of all elements is reduced by a constant factor, and elements whose opacity has fallen so low as to be essentially invisible are removed from the graph. The current value of the opacity is stored in each DOM Node
itself as a new, âbogusâ property. This is optional; you could instead store the value in a separate
data structure keyed by each node (for example, using d3.local()
, which is intended for this purpose), or query the current value using attr()
, update, and reset it.
Example 4-6. Real-time animation (see the left side of Figure 4-4)
function
makeLissajous
()
{
var
svg
=
d3
.
select
(
"#lissajous"
);
var
a
=
3.2
,
b
=
5.9
;
// Lissajous frequencies
var
phi
,
omega
=
2
*
Math
.
PI
/
10000
;
// 10 seconds per period
var
crrX
=
150
+
100
,
crrY
=
150
+
0
;
var
prvX
=
crrX
,
prvY
=
crrY
;
var
timer
=
d3
.
timer
(
function
(
t
)
{
phi
=
omega
*
t
;
crrX
=
150
+
100
*
Math
.
cos
(
a
*
phi
);
crrY
=
150
+
100
*
Math
.
sin
(
b
*
phi
);
svg
.
selectAll
(
"line"
)
.
each
(
function
()
{
this
.
bogus_opacity
*=
.
99
}
)
.
attr
(
"stroke-opacity"
,
function
()
{
return
this
.
bogus_opacity
}
)
.
filter
(
function
()
{
return
this
.
bogus_opacity
<
0.05
}
)
.
remove
();
svg
.
append
(
"line"
)
.
each
(
function
()
{
this
.
bogus_opacity
=
1.0
}
)
.
attr
(
"x1"
,
prvX
).
attr
(
"y1"
,
prvY
)
.
attr
(
"x2"
,
crrX
).
attr
(
"y2"
,
crrY
)
.
attr
(
"stroke"
,
"green"
).
attr
(
"stroke-width"
,
2
);
prvX
=
crrX
;
prvY
=
crrY
;
if
(
t
>
120
e3
)
{
timer
.
stop
();
}
// after 120 seconds
}
);
}
Example: Smoothing Periodic Updates with Transitions
In the previous example, each new data point to be displayed was calculated in real time. Thatâs not always possible. Imagine that you need to access a remote server for data. You might want to poll it periodically, but certainly not for each repaint. In any case, a remote fetch is always an asynchronous call and needs to be handled accordingly.
In such a situation, transitions can help to create a better user experience by smoothing out the time periods between updates from the data source. In Example 4-7, the remote server has been replaced by a local function to keep the example simple, but most of the concepts carry over. The example implements a simple voter model:6 at each time step, each graph element randomly selects one of its eight neighbors and adopts its color. The update function is called only every few seconds; D3 transitions are used to update the graph smoothly in the meantime (see the right side of Figure 4-4).
Example 4-7. Using transitions to smooth out periodic updates (see the right side of Figure 4-4)
function
makeVoters
(
)
{
var
n
=
50
,
w
=
300
/
n
,
dt
=
3000
,
svg
=
d3
.
select
(
"#voters"
)
;
var
data
=
d3
.
range
(
n
*
n
)
.
map
(
d
=>
{
return
{
x
:
d
%
n
,
y
:
d
/
n
|
0
,
val
:
Math
.
random
(
)
}
}
)
;
var
sc
=
d3
.
scaleQuantize
(
)
.
range
(
[
"white"
,
"red"
,
"black"
]
)
;
svg
.
selectAll
(
"rect"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"rect"
)
.
attr
(
"x"
,
d
=>
w
*
d
.
x
)
.
attr
(
"y"
,
d
=>
w
*
d
.
y
)
.
attr
(
"width"
,
w
-
1
)
.
attr
(
"height"
,
w
-
1
)
.
attr
(
"fill"
,
d
=>
sc
(
d
.
val
)
)
;
function
update
(
)
{
var
nbs
=
[
[
0
,
1
]
,
[
0
,
-
1
]
,
[
1
,
0
]
,
[
-
1
,
0
]
,
[
1
,
1
]
,
[
1
,
-
1
]
,
[
-
1
,
1
]
,
[
-
1
,
-
1
]
]
;
return
d3
.
shuffle
(
d3
.
range
(
n
*
n
)
)
.
map
(
i
=>
{
var
nb
=
nbs
[
nbs
.
length
*
Math
.
random
(
)
|
0
]
;
var
x
=
(
data
[
i
]
.
x
+
nb
[
0
]
+
n
)
%
n
;
var
y
=
(
data
[
i
]
.
y
+
nb
[
1
]
+
n
)
%
n
;
data
[
i
]
.
val
=
data
[
y
*
n
+
x
]
.
val
;
}
)
;
}
d3
.
interval
(
function
(
)
{
update
(
)
;
svg
.
selectAll
(
"rect"
)
.
data
(
data
)
.
transition
(
)
.
duration
(
dt
)
.
delay
(
(
d
,
i
)
=>
i
*
0.25
*
dt
/
(
n
*
n
)
)
.
attr
(
"fill"
,
d
=>
sc
(
d
.
val
)
)
}
,
dt
)
;
}
Creates an array of n2 objects. Each object has a random value between 0 and 1, and also knowledge of its x and y coordinates in a square. (The odd
d/n|0
expression is a shorthand way to truncate the quotient on the left to an integer: the bit-wise OR operator forces its operands into an integer representation, truncating the decimals in the process. This is a semi-common JavaScript idiom that is worth knowing.)The object returned by
d3.scaleQuantize()
is an instance of a binning scale, which splits its input domain into equally sized bins. Here, the default input domain[0,1]
is split into three equally sized bins, one for each color. (See Chapter 7 for more detail about scale objects.)Binds the data set and then creates a rectangle for each record in the data set. Each data record contains information about the position of the rectangle, and the scale object is used to map each recordâs value property to a color.
The actual update function that computes a new configuration when called. It visits each element of the array in random order. For each element, it randomly selects one of its eight neighbors and assigns the neighborâs value to the current element. (The purpose of the arithmetic is to convert between the elementâs array index and its (x, y) coordinates in the matrix representation, while taking into account periodic boundary conditions: if you leave the matrix on the left, you loop back in on the right and vice versa; same for top and bottom.)
The
d3.interval()
function returns a timer that invokes the specified callback at a configurable frequency. Here, it calls theupdate()
function everydt
milliseconds and then updates the graph elements with the new data. The updates are smoothed by a transition, which is delayed according to the position of the element in the array. The delay is short compared with the duration of the transition. The effect is that the update sweeps from top to bottom across the figure.
1 See the MDN Event Reference for more information.
2 If you want to access this
in a callback, you must use the function
keyword to define the callback; you cannot use an arrow function.
3 They are: screen
, relative to the edge of the physical screen; client
, relative to the edge of the browser window; and page
, relative to the edge of the document itself. Due to the placement of the window on the screen, and to the scrolling of the page within the browser, these three will generally differ.
4 If you try to implement the current example without using the D3 drag
facility, you may occasionally observe spurious user interface behavior. This is likely the browserâs default action interfering with the intended behavior. The remedy is to call d3.event.preventDefault()
in the mousemove
handler. See Appendix C for more information.
Get D3 for the Impatient now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.