Cooperative Brushing and Tooltips in D3

Sat 23 November 2013

Both brushes and tooltips can be an important part of creating dynamic D3 visualizations. This post is going to give an example of how to use both cooperatively, with a solution inspired by Mike Bostock's remarks here.

First, the finished product (drag to zoom, hover for tooltip):

The visualization starts with a straightforward scatterplot, then tooltips are added via a pattern derived from Chris Viau's excellent work here.

Next we will introduce a general pattern for dynamically brushing and zooming. First, the brush layer is introduced:

var brush = d3.svg.brush()
 .x(x)
 .on("brush", brushmove)
 .on("brushend", brushend);

svg.append("g")
 .attr("class", "brush")
 .call(brush)
.selectAll('rect')
 .attr('height', height);

with some helper functions to manage data and axis transitions on brush:

function brushmove() {
  var extent = brush.extent();
  points.classed("selected", function(d) {
    is_brushed = extent[0] <= d.index && d.index <= extent[1];
    return is_brushed;
  });
}

function brushend() {
  get_button = d3.select(".clear-button");
  if(get_button.empty() === true) {
    clear_button = svg.append('text')
      .attr("y", 460)
      .attr("x", 825)
      .attr("class", "clear-button")
      .text("Clear Brush");
  }

  x.domain(brush.extent());

  transition_data();
  reset_axis();

  points.classed("selected", false);
  d3.select(".brush").call(brush.clear());

  clear_button.on('click', function(){
    x.domain([0, 50]);
    transition_data();
    reset_axis();
    clear_button.remove();
  });
}

function transition_data() {
  svg.selectAll(".point")
    .data(data)
  .transition()
    .duration(500)
    .attr("cx", function(d) { return x(d.index); });
}

function reset_axis() {
  svg.transition().duration(500)
   .select(".x.axis")
   .call(xAxis);
}

Be careful here: if you append your brush after your data points, the brush overlay will catch all of the pointer events, and your tooltips will disappear on hover. You want to append the brush before your data points, so that pointer events on the data generate a tooltip, and those on the overlay generate a brush. See that here.

Now you have another problem: any brushes that start on the data points don't work, because the data/tooltip is catching the pointer event. This might not be an issue for plots with sparse data, but for data-heavy plots you need that event to propagate through to the brush.

You can solve this by creating your own event and dispatching it to the brush layer:

points.on('mousedown', function(){
  brush_elm = svg.select(".brush").node();
  new_click_event = new Event('mousedown');
  new_click_event.pageX = d3.event.pageX;
  new_click_event.clientX = d3.event.clientX;
  new_click_event.pageY = d3.event.pageY;
  new_click_event.clientY = d3.event.clientY;
  brush_elm.dispatchEvent(new_click_event);
});

This final step will result in the chart at the top of this page- notice that you can click anywhere on the chart and have the brush and tooltips work cooperatively. If you don't want tooltips to show up while brushing, try setting pointer-events: none on your data elements in the mousedown function above.

This entry was tagged as brush tooltip D3