CS 465 - Tutorial Five
Objectives
- Learn about transitions – the fundamental tool of animation in D3
- Get some practice working with the Enter, Update, Exit join pattern
Prerequisites
Create the git repository for your tutorial by accepting the assignment from GitHub Classroom. This will create a new repository for you with a bare bones npm package already set up for you.
Clone the repository to you computer with
git clone(get the name of the repository from GitHub).Open the directory with VSCode. You should see all of the files down the panel on the left in the Explorer.
In the VSCode terminal, type
pnpm install. This will install all of the necessary packages.In the terminal, type
pnpm devto start the development server.
Transitions
As we saw in class, The basic pattern to creating transitions is - make a selection - call transition() on the selection - use functions like delay() and duration() to configure the transition - use attr() and style() to describe the state of the selection when the transition is concluded
A Local Haunting
While all of the things we do in this tutorial can be applied to “proper” visualizations, I thought in honor of the season we would have a little bit of fun. So, we are going to animate a little ghost.
I’ve given you the code to display the ghost below. You can see that the basic structure is essentially the same thing that we have done to build other visual structures. This time we are using the use tag, which has attributes x, y, width, height, and href (the source of the image). This allows us to embed one SVG inside of another one.
I have already loaded the ghost svg into the file and given you this code:
const width = 500;
const height = 100;
const svg = d3.select("#ghost01")
.attr("viewBox", `0 0 ${width} ${height}`)
.style("width", `${width}px`)
.style("height", `${height}px`)
.style("background", "#222");
addGhostToSVG(svg, ghostContents, "ghost-img");
const ghost = svg.append("use")
.attr("href", "#ghost-img")
.attr("x", 25)
.attr("y", 25)
.attr("width", 75)
.attr("height", 75)This will place the ghost into the SVG region above the code block.
I’ve provided a function for you called addGhostToSVG. The ghost is an SVG image, stored in a .svg file. There are a variety of ways for us to incorporate an SVG image into an existing SVG. We are storing the image in the defs tag of the SVG. Once it is loaded in there, we can use the use tag and provide the id of the stored image to show the image within the SVG. We can even show multiple copies of the image in the same SVG this way. You can read through the function to see all of the steps required to load the ghost image into the SVG.
The value stored in the ghost variable is a selection (albeit a selection with only one item in it). As we previously discussed, each call to attr or style returns the same selection so I can save the selection in a variable despite the method chaining.
We are going to add a transition to the selection. This is as easy as writing ghost.transition(). Of course, this won’t actually do anything yet. Let’s make the ghost glide across the SVG region. After the transition, set the x attribute to 425.
Run the cell. Our ghost will rocket from one side to the other. We can control the speed with duration.
Add a duration(2000) call to the transition. This will set the length of the transition to 2 seconds (or 2000 milliseconds). It doesn’t matter where this goes, but I think it is easier to read if the duration comes before the attr.
As you work, you may notice that you have multiple ghosts in the haunted region. Every time you save your file, Observable Framework will hot reload the page, which re-runs the code, but it doesn’t reload the page. As a result, your code is appending a new ghost to the SVG that already contains one (or more). We will ignore this for the time being, you can always manually reload the page to clear it out.
Easing
You will notice that the speed of the ghost is not constant. It speeds up and then slows down. This is controlled by the easing function. The idea is that animations look more natural if they ease in and out rather than slamming around at a constant rate. The default is to use d3.easeCubic, but we can set it to other functions using the ease function.
Add .ease(d3.easeLinear) to the transition.
The ghost should now move at a constant rate.
Now try .ease(d3.easeBounceOut).
There are a lot of different easing functions. This notebook illustrates what they all look like in action.
Pick one that you feel suits the ghost.
Delay and chaining
We can delay the start of a transition with the delay function. It works about as you would expect.
One use of the delay is to build multi-part animations. If you call transition() again on a transition, it creates another configurable transition. We can set a delay on the transition so it happens after the first one starts.
Add a new transition to the ghost that waits for two and a half seconds and then uses style("opacity", 0) to make the ghost fade away. Configure the fade to something sufficiently ghostly.
Once this is working, add two more transitions. The first should shoot the ghost back to the start (still invisible), and the second should fade it back into view again (this is basically what is happening with the balls on the ease example notebook).
Feel free to play around and add some more transition to get a feel for how they work.
Joins
The second thing we are going to look at in this tutorial is the mechanics of the data join.
As we discussed in class, when we do a join, there are really three possibilities
A data item may not match with any existing DOM element. This is certainly been the case when our selection of DOM elements has been empty. It can also happen even if we have some DOM elements in the initial selection if those elements are already bound to different data.
A DOM element might be in the initial selection, but there is no matching data element for it. This can happen because we have removed the data item previously bound to the DOM element from our data set.
We can match the data item to a DOM element that was previously bound to it.
To accommodate these three possibilities, the join function actually creates three selections:
- enter (data for which there is not yet a DOM element)
- exit (DOM elements for which there is no longer any data)
- update (DOM elements that have been paired up with data)
The selection example does a pretty good job of walking you through the mechanics of this, and we worked through an example of this in lecture.
Release the ghosts!
Let’s return to our theme of using these tools in a more seasonal / less visualization way. We are going to create a little graveyard where ghosts can wander around. From time to time, new ghosts will fade into view. The other ghosts are skittish and will run away from it before resuming their aimless wandering. If a ghost strays too near the boundaries of our graveyard it will fade away.
The “data” behind this visualization will be an array called ghosts containing objects with four attributes: id, x, y, and heading.
I have provided most of the code. Take a moment to familiarize yourself with what is going on. In src/index.md, I have provided you with the SVG region (the “graveyard”) and loaded our ghost image into it. There is also a button hooked up to start and stop our animation. The actual animation loop is set up with a call to d3.interval(f, time), which continuously called the function f every time milliseconds.
The rest of the functions can be found in src/components/functions.js. The code I have provided handles the data side of things. On every cycle, it
- clears out ghosts too near the border
- moves the ghosts based on their current heading and then tweaks their heading
- occasionally adds a new ghost (from which all of the other ghosts flee)
The ghost data is constantly being tweaked, so this returns the ghost data upon completion so that we can build on it in the next interval.
If you click the start button, all kinds of things will happen, but you won’t be able to see them because I didn’t provide any code for visualizing what the ghosts are getting up to. Your job will be to fill in the updateDisplay function so we can see the ghosts.
You will see that I have given you the selection containing the SVG region and the ghost data.
Start by adding the selectAll/ data / join calls.
For the selectAll, instead of selecting a tag, we are going to select “.ghost”. As a reminder, we are using CSS selector syntax. This time, instead of selecting an element type (like use), we are going to select all members of the class “ghost” (note that the class name is “ghost”, the selector is “.ghost”).
For data, you will pass in the ghosts data. You will also want to add a key function that returns the ghost’s id.
Enter selection
For the join, we are going to specify the behavior of our three selections. For the moment, however, we are just going to worry about the enter selection, so for the other two arguments to the join function I would like you to put in the identity function (i.e., update=>update and exit=>exit).
For the enter selection, I would like you to append a new ghost. Recall how we made the first ghost:
.append("use")
.attr("href", "#ghost-img")
.attr("x", 25)
.attr("y", 25)
.attr("width", 75)
.attr("height", 75);You can largely do the same thing, but we have some changes to make. - We now have a variable called GHOST_SIZE that you should use for the width and height attributes. - For the position, you should use accessor functions to set the position based on the data - I would like you to add a “class” attribute and set it to “ghost” (so our selection is not a lie)
If you run the function, ghosts will start popping up in the SVG region and then just sort of sit there.
Before we address the immortal ghosts, let’s make the initial behavior a little more ghostly. Set the opacity of the ghost to 0 and use a transition to fade the ghost in.
Exit selection
Okay, let’s make the ghosts disappear.
You may have noticed in the selection example above that we can call remove on the exit selection. This removes the DOM elements from the page. Add a remove() to the exit selection.
Now the ghosts will occasionally just vanish.
This seems a little abrupt. Add a transition before the remove and make your ghost fade away before it is removed. For a slightly different effect, at the same time that you turn the opacity to 0, set the width and height to 0 as well so the ghost shrinks and fades at the same time
Test it out. You should see ghosts fading away after a while now.
Update selection
The final piece is to get the ghosts to move around. Of course, in the data, they are moving all over the place, we just haven’t visualized it yet.
To address the change in the data, we are going to be dealing with the update selection. Since the position of the ghosts has changed, add attr calls and set the x and y positions based on the new value in the data.
If you run this (run this), you will see the ghosts sort of pop around the space. At this point, you know where this is going… Add a transition to make the ghosts glide from place to place.
There are some aspects of the final effect that we could address. The d3.interval time fires every second (1000 milliseconds). You want the duration of the update transition to be very similar. if the duration is longer, the transition will be cut off before it is complete. If the duration is shorter, the ghosts will pause before the next animation step. Experiment a little bit. I have found the best compromise is just a little longer than the timer’s duration.
The other jarring piece is that since we can’t completely remove the pause between steps of the ghosts, it is obvious that they all move together. If you add a staggered delay like the one we used in lecture, you can at least have the ghosts setting out at slightly different times.
Play around with those and the easing functions to get something you like.
Final thoughts
Okay, that’s it! You should now have a collection of somewhat skittish ghosts roaming around in your web page (and have hopefully learned something about selections, joins and transitions along the way).
Reflection
Before you submit, make sure to answer the questions in reflection.md.
Submitting your work
- Commit your work to the repository (see the git basics guide if you are unfamiliar with this process)
- Push the work to github
- Submit the repository on Gradescope (see the Gradescope submission guide). For this tutorial there will not be any automated tests.