# CS465 - Lab 6 Brushes

# Objectives

  • Learn about brushes in D3

# Starting the lab

Go to Github Classroom and grab the starter code for the lab.

# Creating a brush in D3

For the first part of this exercise, we will add a simple brush to a scatterplot. If you open up simple_brush.html from the repository, you will find a familiar scatterplot. We are once again working with the Iris dataset. This time I've given you the scatterplot, and even colored it in based on the class of the plants (take a look at the color scale I used for ideas for your homework...).

We are going to add a brush to the visualization. It won't do anything terribly interesting; the selected items will remain colored, while the data points outside of the selection will turn light grey.

# D3 brush

The brush in D3 works much like the axis. We create a function, configure it, and then call it on the region we would like the brush to live in. As with the axis, this creates a g and populates it with some visual elements (the primary of which is a rectangle that forms the visual representation of the brush). We can control its appearance through CSS, and we can also limit it to a single dimension if we like. If we ever want to reconfigure it, we have to call the brush function on the context area again (just as we do with the axes).

Once the brush is registered on an SVG region, the cursor will turn to a cross-hair, and we can draw out a selection rectangle in the usual way. Once made, the selection can be moved around, and even resized from "handles" on the side.

To interact with the brush, we have a collection of events we can listen for (start, brush, and stop). The start event happens at the start of any brush activity, the brush event is fired continually while the action is carried out, and the stop event is called when the action is complete. So, for example, drawing a selection box will follow this sequence: The mouse button is pressed, and the start event is fired. The mouse starts to move, and for each change in position, the brush event is fired. Finally, the mouse button is released and the stop event is fired.

Inside our callbacks, we can get the bounds of the selection with d3.event.selection. For a 2D selection, this will be an array of the form [[x0,y0],[x1,y1]], where x0 and y0 are the minimum x and y values respectively, while x1 and y1 are the maximum x and y values. It is up to us to translate this into the data space to figure out which elements are actually in the selection area.

# Add the brush to the plot

Here are the steps to create our simple brush. Unless otherwise specified, put the code at the end of the then, right after the lines that draw the axis labels.

Step one Create the brush. Write const brush = d3.brush().extent([[0,0], [width,height]]);. The first part of this creates a new brush. The call to extent creates boundaries so that the selection rectangle is restricted to just the plot (otherwise you will be able to draw the selection box on the wrong side of the axes and other such foolishness).

Step two Add the brush to the chart. Append a g to the chart. Give it the class "brush" and then use the call() function to call the brush we just made (i.e., just pass brush as the argument to call).

Step three Test. You should now be able to draw the selection rectangle, move it around, resize it, etc.

Step four Add a "brush" event handler. Do this between the creation and the deploying (it will work if you do it after the deploying, but I want you to get in the habit of thinking: create, configure, deploy). As with our other event handlers, you will call brush.on(target, handler). For this, we will handle the "brush" event as the target. In the handler, print out d3.event.selection.

Step five Adjust the selection to data space. If you look carefully at the console output from the last step, you will see that the selection extent is reported in pixels. We need to convert this to numbers in our data space. Fortunately, our scales have an invert function which takes value from the range and returns their equivalent in the domain. To make my life a little easier, I stored the d3.event.selection in a variable called extent. I then created a variable like this: let x_extent = [extent[0][0], extent[1][0]].map(x_scale.invert);. This grabs the two x values and puts them in an array. Then it calls the map function, which applies a function to all of an array's elements and returns a new array of the results. In this case, it applies the invert function. Do the same for the y values.

Step six Change the color of the items not in the selection. We are going to approach this like we did changing colors on mouseover. In the style tag at the top of the file, add a new rule:

.deselected{
        fill:lightgray !important;
    }

This creates a style for a class called "deselected", and sets the fill to "lightgray". The !important is key. This will allow the style to override the coloring we applied when we created the circles.

Then, back in the "brush" handler, add a line that calls classed on circles. Recall that classed takes a class name (in this case "deselected"), and an accessor function that returns a Boolean value. Your function should return true when the petal length of the item is less than the smaller x of the extent or larger than the largest x or the sepal length is similarly outside of the y extent.

Step seven Test. The color of the circles should now reflect the current selection.

Step eight Fix the clear behavior. If you just click on the plot, the selection rectangle will go away, but the coloring will not change. This is because the "start" and "end" events fired off, but not the "brush". The selection rectangle is now empty, but our code didn't see it happen. We will fix this by adding another handler for the "end" event. Add a condition that tests if d3.event.selection is equal to null. If it is, used classed to set the "deselected" class to false for all circles.

# Time to supersize

Just coloring selections isn't very interesting in a single plot. It becomes more interesting when we add linking to the brush, so the selections are shown in other plots so we can see relationships.

If you open up matrix_plot.html, you will find that I have created a matrix scatterplot for you of the iris data. Go ahead and read through to see how it works.

Your job will be to add brushes to the plots. At the end, you should be able to select any region and the selection will show in all subplots. Unlike our dashboard, we will not maintain multiple selections, so creating a selection in any subplot will clear any existing selections.

Step one Add in a brush for every plot. You will see I left a comment in place where you should add the brushes. We could just add a single brush for the entire chart, but that makes it more difficult to limit where it can validly draw. use the same steps as before to create a brush and call it on the chart. This time, use chart_size for the extent. Once you have done this, you should be able to draw selection rectangles in all plots, and they won't drag outside of the plot bounds.

Step two Add a "brush" event handler. This will basically be the same as the one we wrote for the simple version. The only difference is that the variables assigned to the x and y axes are dependant on the current iteration of the loop (look at how cx and cy are assigned -- note how both the variable and the scale are based on loop iteration). The other difference is that you don't want to use the circles variable, which only contains the circles in the current sub-plot, you want to select all of the circles in the visualization.

There is something very weird here. If you use x_metric in the handler, surely the handler will only use the last value of x_metric. How will it know which one to use? This is the power of closures. Since we are essentially re-making this function on every pass through the loop, the function retains the scope of the loop at the moment it was created. Note that this doesn't work if x_metric was declared with a var instead of a let

Step three Handle the clearing of the selections. We are going to handle the clearing in the "brush" event handler this time because of the next step. So, after you create the extent variable, add a condition. If the extent is null, set "deselected" to false for all circles, otherwise, find the data extents and color appropriately.

Step four Limit the visualization to a single brush. Currently, you can create selections all over the place, and the coloring rapidly gets out of sync with the selection rectangles. We are going to fix that by clearing out any active selections before starting a new one. Create a new event handler for "start" events.

All updates to brushes are done with brush.move. This allows us to programmatically resize and move brushes around. To "clear" the brush, we are going to set the selection extent to null. To do that we take D3 selection containing a brush and call(brush.move, null) on it. We want to clear all selections, so we can use selectAll('brush') to grab them all. Add this line in, but don't try selecting anything yet.

The problem is that move generates brush events (it will, in fact, issue all three events). So, if our "start" handler tells all of the brushes to move, all of those move calls will generate "start" events, which tell all of the brushes to move, which,... well, you can see where this is going. So, we need to add a guard to make sure the event was generated by a human. Check this condition before calling move: 3.event.sourceEvent.type === "mousedown". This will make sure that the event we are looking at was actually caused by someone with a mouse, and not from some JavaScript.

Step five Test.

# (Optional) Making things pretty

I left off the axes and the axis labels. They aren't required for full credit on the lab. However, a good test of your understanding of D3 would be to add these in. You could get fancy and put the labels in the plot son the diagonal. Or you could even get super fancy and special case the diagonal and put in histograms like I showed you when I introduced matrix scatterplots.

Last Updated: 10/29/2018, 1:00:03 PM