D3 and DOM Events
FREE     Duration: 13:16
Part of Course: Intermediate D3 Course
 

Takeaways:

  • d3.selection.on(type[, listener[, capture]]) adds an event listener to each element in the current selection
  • D3 invokes the listener in the same way it invokes other D3 operator functions - by passing the current datum "d", index "i", and the "this" context as the current DOM element
  • Notice D3 allows you to take advantage of capture phase versus bubbling phase event triggers
  • d3.event captures an event when it happens and stores it in the variable d3.event
  • The d3.event variable is a global variable that can be used in the event listener callback function registrered with the d3.selection.on operator
  • After the JavaScript callback function has finished running, the current d3.event variable is reset
  • You can "delete" an event listener by passing the JavaScript null object as the function for the eventListener type

Transcript:

D3 and DOM Events

D3 selection.on


Now that we have covered how to add an event listener to an HTML element using DOM Level 2 Event Listeners with JavaScript, let's cover how we can do the same with thing D3.

target.addEventListener(type, listener[, useCapture]);


The d3 selection dot on method adds an event listener to each element in the current selection.

target.addEventListener(type, listener[, useCapture]);

d3.selection.on(type[, listener[, capture]])

So we can use select for a 1 element selection or a selectAll for a multi-element selection.

The type is a string of the event type that we want an event listener for.

Examples could be mouse down, mouse up, mouse over, mouse out, etc.

D3 invokes the listener in the same way it invokes other operator functions - by passing the current datum d and index i and the "this" context as the current DOM element.

Note that we can still take advantage of capture phase vs bubbling phase event triggers in D3.


This example creates a selection of all the SVG Circle Elements.

d3.selectAll("circle")
    .on("mouseover", function(d,i) { alert("mouseover"); })
    .on("mouseout",  function(d,i) { alert("mouseout");  });

Then using the d3 selection dot on, it adds a mouse over event listener to each SVG circle element.

When the mouse goes over one of the SVG Circle Elements, the anonymous function will be invoked.

Currently, it will alert us with the string "mouseover"

Then using the D3 selection dot on, it adds a mouse out event listener to each SVG circle element.

When the mouse leaves one of the the SVG Circle Elements after having been on it, the anonymous function will be invoked.

Currently it will alert us with the string "mouseout"

Notice that we are adding two different types of event listeners to the same SVG Element.

Also notice that every single element in the selection will receive the same event listeners.


As you can imagine, we can also use the index and the data object attached to each circle element.

d3.selectAll("circle")
    .on("mouseover", function(d,i) { alert( d.value ); })
    .on("mouseout",  function(d,i) { alert( d.value );  });

In this example, if the data object had a key of "value", then this would return the value of the key,value pair.

This is very powerful, because now we have some interaction with the mouse.

This is one of the reasons that we have covered before, why it's very important to leave the data unchanged when attaching it to DOM elements.

We can also use the element index as well as the "this" context of each element.


Because event interactions can get very complicated quickly with transitions and the duration of the transition, styling and other functionality, it's often easier to understand if you write named functions elsewhere in the code that the selection.on can reference.

function mouseover(d) { alert(d.value); }

function mouseout (d) { alert(d.value); }

d3.selectAll("circle")
    .on("mouseover", mouseover)
    .on("mouseout",  mouseout );

In this way, your code will be very clear to read and be maintainable.


SVG DOM Element + Mouse Event

So far so good, we are able to attach an event listener to the SVG Dom Element.

We are also able to specify specific actions / calculations that must happen when the event is triggered through a JavaScript Function.

Let's take a look at a few examples in the JavaScript Console.


Let's start simply by interacting with HTML Elements.


We create 3 paragraph elements.

d3.select("body").append("p").text("one").attr("id","p_1");

d3.select("body").append("p").text("two").attr("id","p_2");

d3.select("body").append("p").text("three").attr("id","p_3");

You can see the three paragraphs in the browser window.

You can see their id's in the Chrome Developer Tool's Elements Section.


Next, let's select the first paragraph and add a mouseover event to it.

d3.select("#p_1")
    .on("mouseover", function(d, i) { alert("paragraph power!");});

Before we move our mouse over the paragraph, notice that once the command was run nothing was added to the HTML of the page.


Now, let's move over the paragraph.

BROWSER move over any / all the paragraphs, showing that only the first paragraph has a pop-up.

BROWSER click okay on the pop-ups to close them.

You should have seen the pop-up happen.

That shows you how easy it is to add events to DOM elements, in this case HTML with D3.


Next, instead of selecting the first paragraph, let's create a selection of all the paragraphs and add the same on mouseover event functionality.

d3.selectAll("p")
    .on("mouseover", function(d, i) { alert("paragraph power!");});

This will add a mouseover event listener to every paragraph element in the selection.

For now every paragraph will exhibit the same behavior - have an alert pop up with the string "paragraph power!" when we mouseover the paragraph.


Let's move the mouse over the paragraphs.

BROWSER move over any / all of the paragraphs.

BROWSER click okay on the pop-ups to close them.

You can see that our event trigger now happens for every paragraph element.

You can also see that all the paragraphs exhibit the same behavior.


We reset the browser and this time we'll generate some paragraphs with data.

BROWSER reset


Let's start with a simple array of 5 numbers.

var myData = [5, 4, 3, 2, 1];


Let's create paragraphs with them.

d3.select("body").selectAll("p").data(myData).enter().append("p");

d3.selectAll("p").text(function(d, i) { return "paragraph " + i; });

Browser - Click into the ELEMENTS section and open the body / and first paragraph.

You can see the output, which is 5 paragraph elements with text inside of them.

D3 selections are 0-indexed, so the text of the paragraphs is paragraph 0 all the way to paragraph 4.


Let's check the first paragraph to see what datum was bound to it.

d3.select("p").data();

This tells us the datum attached to the first paragraph is 5.

This is what we expect.


Let's now add a mouseover function to each paragraph that tells us the datum that is attached to it.

In order to do that, we'll have to add a D3 selection.on event.

d3.selectAll("p")
    .on("mouseover", function(d, i) { alert(d); });

This will add an event listener to each paragraph element in the selection.

When an element registers a mouseover event, it will run the anonymous function we defined.

In this case, it will alert us to the datum bound to the element.

Note - that since we did not set or use the capture flag, the events will be triggered during the bubbling phase.


Let's move the mouseover the paragraphs

BROWSER move over the paragraphs from bottom to the top.

BROWSER click okay on the pop-ups to close them.

You can see that each paragraph has an event trigger that happens for every element.

You can also see that the behavior is the same for all the paragraphs, though the results are different because we are using a function to access element specific data for each event.


Now, let's make the function that is triggered more complicated and move it outside of the selection.on definition.

function paragraphMouseOver(d, i) {
    if (i % 2 === 0) { alert(i + " is an even index, datum: "+d); }
    else { alert(i + " is an odd index, datum: "+d); };
};

This function will take in the d and i variables that are available from D3.

It will then use the i to test to see if the element has an even or odd index.

If it is even, then it will alert us with a string that tells us it is even and the datum attached to it.

If if is odd, then it will alert us with a string that tells us it is odd and the datum attached to it.


Now, let's re-define the event listener.

d3.selectAll("p")
    .on("mouseover", paragraphMouseOver);

Before we test out the new event listener, you may be wondering what is going to happen to the previous event listener that we defined.

The D3 API tells us that if an event listener was already registered for the same type of event on the same selected elements, the existing listener is removed before the new listener is added.

If we really want to have multiple event listeners for the same element event type, the type can be followed with an optional name space.

We will leave that for later.


Alright, let's mouse over these paragraphs.

BROWSER move over the paragraphs from bottom to the top

BROWSER click okay on the pop-ups to close them.

You can see that each paragraph element has a new event listener function.

You can see that the behavior is the same for all the paragraphs based on the function paragraphMouseOver.

However, you can see the paragraphs are displaying different results based on their data and index.


Let's turn our attention to SVG DOM Objects and giving them event power.


As you can imagine, it works the exact same way.


This is why it's helpful to think of D3 as more than a Data Visualization JavaScript library - because we can do some wonderful amazing things with regular HTML.


Next, let's create three SVG circle elements based on data.


This time, our data source will be more complicated than the 1 dimensional array of numbers, it will be an array of JSON objects.


We reset the browser.

Then we define the three circles


var circleData = [ { "x": "050", "y": "050", "r": "30", "color": "yellow", "fruit": "banana"},
                   { "x": "100", "y": "100", "r": "30", "color": "purple", "fruit": "grape" },
                   { "x": "150", "y": "150", "r": "30", "color": "red",    "fruit": "apple" }
];

You can see that each JSON object has an x coordinate, a y coordinate, a radius, a color and a fruit type.

We will use all of this data in our circles and events.


Next, we create the circles.

var svg = d3.select("body").append("svg");

var circles = svg.selectAll("circle").data(circleData).enter().append("circle");

BROWSER - open the body and SVG element to show the three circles.

First, we create the SVG Viewport that the SVG Circle elements will live in.

Then we create the circles with data bound to them.

The three circles now appear in the Chrome Developer Tool's Elements section.


Next, let's double check to make sure each circle got the right data.

circles.data();

BROWSER - open all three array and objects

As you can see, the circles have the JavaScript objects bound to them.


Next, let's add the attributes to the circles.

circles
    .attr("cx",function(d,i) { return d.x; })
    .attr("cy",function(d,i) { return d.y; })
    .attr("r", function(d,i) { return d.r; })
    .style("fill",function(d,i) { return d.color; });

We add the attributes for the "cx", "cy" and "r" to construct the circles.

Then we add a style fill to color the circles.

You can now see the three circles based on their data.


Now, let's add an event to all the circles that tells us the type of fruit they represent.

We will do it with an anonymous function within the D3 selection.on definition.

d3.selectAll("circle")
    .on("mouseover", function(d, i) { alert(d.fruit); });

This code will add an event listener to each circle and will alert us of the type of fruit the circle represents.

Let's run our mouse over the circles.

BROWSER mouse over the circles.

BROWSER click okay on the pop-ups to close them.

You can see that the events are in fact attached to each circle element and each one has a mouseover event listener function.


Next, let's try adding another event listener, this time a mouseout event.


We try the mouseout event here, because these circles have a wide enough radius that it is easy to see that it will only trigger when the mouse is no longer over the circle.

d3.selectAll("circle")
    .on("mouseout", function(d, i) { alert("is tasty!"); });

We use the string "mouseout" as the event when the mouse is no longer over the element we have defined the event listener for.

Each time the mouseout event is triggered, each element will alert us with a string of "is tasty".


Let's try it out by mousing over and then moving the mouse away from the circles.

BROWSER mouse over a circle

BROWSER press enter.

BROWSER mouse away from a circle

BROWSER press enter.

As you watch the mouse go over the circle, you can see the alert pop up telling us the specific fruit the circle represents based on the data bound to the specific SVG Circle element.

We can move the mouse anywhere on the circle and no other event will trigger.

When we finally move the mouse away from the circle, a new event triggers which has the anonymous function alert us with the string "is tasty".

With these types of interactions, you can see how this can lead to very interactive data visualizations.


Now that we have worked through adding events listeners to various DOM elements, let's take a closer look at the actual event that is moving through the capture and bubbling phase.



D3.event


DOM Event Properties

The Document Object Model Event, when triggered, has a number of event properties that are helpful in different types of scenarios.

Some of these properties include the following:

A time stamp of when the event occurred

The horizontal coordinate of the event relative to the whole document

The vertical coordinate of the event relative to the whole document

And other properties that help answer questions like

What is the type of the event?

Which DOM element is the target of the event?

Which key was pressed during the event?

Which mouse button was pressed during the event?

and What was the mouse position during the event?


D3 captures an event when it happens and stores it in the variable d3.event. - d3.event

This is a global variable that can be used in the event listener callback function registered with the d3 selection dot on operator.

After the JavaScript callback function has finished running, the current d3.event is reset.


There are a variety of reasons you may want to capture event properties.

d3.event.pageX

d3.event.pageY

One that comes up frequently is building tool tips.

In order to build a tool tip, we have to know the x and y over the event that happened, so that we can build the tool tip close to the DOM element.

From the current event object we can extract the X and Y coordinates of the event.

These X and Y coordinates are where the event happened, which means it is where the mouse currently is.

This is useful when we are making a tool tip to show the data of a specific element.


D3 selection.on()

d3.event // <--- GLOBAL VARIABLE
// d, i, this AND event

Going back to the D3 selection.on method.

It is important to emphasize that because the d3.event is a global variable, that we can use the event within the functions we define for when an event triggers.


Continuing with our three circles, let's work on getting the pageX and pageY properties out of the event.

First though - let's cover how we can "delete" an eventListener.

That is, how can we use D3 to turn off an event we no longer want.

In this case, we want to turn off the "mouseout" event.


The way to do this with D3 is to pass the null object as the function for the eventListener type.

circles
    .on("mouseout",null);

BROWSER - mouse over a circle

BROWSER - press okay on the alert box

BROWSER - mouse out of the circle

BROWSER - repeat with each circle.

You saw the event when we moved the mouse over the circle.

However, when we moved the mouse away from the circle, nothing happened.

So you can see that the way to "delete" or "turn off" an event is to redefine it with the null object.


Alright, now that we only have the mouseover event, let's extract an event property from it.


We will redefine the "mouseover" eventListener to alert us of the x and y coordinate the event occurred at.

circles
    .on("mouseover", function(d, i) {
        alert("event happened at x:"+ event.pageX + " y:"+ event.pageY);
    });

BROWSER - go in and out of the circles.

You can see that every time we mouseover a circle, we get an alert as we did before.

This time however, we are getting the x and y coordinates for where the event happened.


So you can see that not only can you define event listeners within the actual functions, we can use the event object as well.

<< Back To D3 Screencast and Written Tutorials Index