Clicking and hovering

D3 was probably sold to you under the banner of interactive visualization! And yet, hiding somewhere in the back of your head, you’re saying to yourself come on dude, we haven’t done a single goddamn interactive thing. So today we’re going to do those goddamn interactive things.

Mouse events

When your mouse does something to an element on the page - hovers over it, clicks it, etc etc etc - these are called mouse events. D3 supports mouse events just the same as it supports everything else!

Clicking on a data point works like this:

rectangles.on('click', function(d, i) {
  console.log("You clicked", d), i;
})

And hovering onto a data point works like this:

rectangles.on('mouseover', function(d, i) {
  console.log("Your mouse went over", d, i);
})

d is (as always) the data point, and i is (as always) the index of the element you’re looking at.

Not so bad, right? Take a look at the following code, then open up the JS console to see what’s happening as you move your mouse over the circles.

Open example in new window

<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var height = 200,
    width = 400;
var svg = d3.select("svg").attr('height', height).attr('width', width);

var max_words = d3.max(datapoints, function(d) { return d['words']; });
var x_scale = d3.scale.linear().domain([1800, 2015]).range([0, width]);
var y_scale = d3.scale.linear().domain([0, 500000]).range([height, 0]);

svg.selectAll('circle')
    .data(datapoints)
    .enter()
    .append('circle')
    .attr('r', 10)
    .attr('cx', function(d) { return x_scale(d['published']) })
    .attr('cy', function(d) { return y_scale(d['words']) })
    .on('click', function(d, i) {
      console.log("click", d);
    })
    .on('mouseover', function(d, i) {
      console.log("mouseover", d);
    })
    .on('mouseout', function(d, i) {
      console.log("mouseout", d);
    })
</script>

Open example in new window

But that stuff’s not so fun, what’s fun is when you can change the element you’re clicking or hovering on.

In order to make a change, you need to use this thing called this. Don’t ask me what it is or anything, just be a good kid and use console.log(this); and look at it.

NOTE: You could I guess read something like this if you wanted.

this in this particular instance is the circle you’re clicking or the rectangle you’re hovering or whatever random element you’re interacting with. As a result, you can use d3 to select is and then change its attributes, just like anything else

.on('click', function(d, i) {
  d3.select(this)
    .transition()
    .attr('r', 20);
})
.on('mouseover', function(d, i) {
  d3.select(this)
    .transition()
    .attr('fill', '#ff0000');
})

Try clicking and hovering on the elements below.

Open example in new window

<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var height = 200,
    width = 400;
var svg = d3.select("svg").attr('height', height).attr('width', width);

var max_words = d3.max(datapoints, function(d) { return d['words']; });
var x_scale = d3.scale.linear().domain([1800, 2015]).range([0, width]);
var y_scale = d3.scale.linear().domain([0, 500000]).range([height, 0]);

svg.selectAll('circle')
    .data(datapoints)
    .enter()
    .append('circle')
    .attr('r', 10)
    .attr('cx', function(d) { return x_scale(d['published']) })
    .attr('cy', function(d) { return y_scale(d['words']) })
    .on('click', function(d, i) {
      console.log("clicking on", this);
      // transition the clicked element
      // to have a radius of 20
      d3.select(this)
        .transition()
        .attr('r', 20);
    })
    .on('mouseover', function(d, i) {
      console.log("mouseover on", this);
      // transition the mouseover'd element
      // to having a red fill
      d3.select(this)
        .transition()
        .attr('fill', '#ff0000');
    })
</script>

Open example in new window

Notice how the circles stay red after you stop hovering? That’s because mouseover isn’t exactly the same as what you’d think “hover” means - instead, it’s what should happen when you mouse moves on top of the element. It doesn’t care at all about when you move off.

Of course, another event does - mouseout! Easy enough to remember, right? To do a hover, more or less you need to have mouseover make the hover effect and then have mouseout remove it.

Let’s add a hover effect that goes away when you mouseout the element.

Open example in new window

<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var height = 200,
    width = 400;
var svg = d3.select("svg").attr('height', height).attr('width', width);

var max_words = d3.max(datapoints, function(d) { return d['words']; });
var x_scale = d3.scale.linear().domain([1800, 2015]).range([0, width]);
var y_scale = d3.scale.linear().domain([0, 500000]).range([height, 0]);

svg.selectAll('circle')
    .data(datapoints)
    .enter()
    .append('circle')
    .attr('r', 10)
    .attr('cx', function(d) { return x_scale(d['published']) })
    .attr('cy', function(d) { return y_scale(d['words']) })
    .on('mouseover', function(d, i) {
      console.log("mouseover on", this);
      // make the mouseover'd element
      // bigger and red
      d3.select(this)
        .transition()
        .duration(100)
        .attr('r', 20)
        .attr('fill', '#ff0000');
    })
    .on('mouseout', function(d, i) {
      console.log("mouseout", this);
      // return the mouseover'd element
      // to being smaller and black
      d3.select(this)
        .transition()
        .duration(100)
        .attr('r', 10)
        .attr('fill', '#000000');
    })
</script>

Open example in new window

Not so bad!

Ideas for interaction

For a little inspiration, check out there two talks on making videogames more exciting: juice it or lose it and the art of screenshake.

  • Display a tooltip on hover or click
  • Add a little color to hovered elements to engage the user
  • Sorting
  • Filtering
  • Going deeper into multi-level into the data

You get the idea.

Want to hear when I release new things?
My infrequent and sporadic newsletter can help with that.