Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3.js Brush Controls: getting extent width, coordinates

I'm using d3.js, and was wondering how I might get the sides, width, coordinates, etc; of the extent. In an example like this http://bl.ocks.org/mbostock/1667367

like image 933
AgentElevenFifths Avatar asked Apr 04 '14 21:04

AgentElevenFifths


1 Answers

Brush.extent()

When using a brush control, you access information about the state of the brush using the .extent() method on the brush object.

The information returned by the .extent() method depends on what sort of scale(s) you have connected to the brush object.

If you have one scale linked (either an X-scale or a Y-scale, but not both), then the extent method returns a two-element array of the form [minimum, maximum].

If you have both X and Y scales attached to the brush object, then the extent method returns a nested array of the form [‍​[xMinimum, yMinimum], [xMaximum, yMaximum]​].

But what are these minimum and maximum values? That also depends on the scale. If the scale has a valid .invert(value) method, the minimum and maximum values will be converted into your data domain values. For ordinal, threshold and other scales which do not have a simple invert method, the brush function returns the values in the coordinate system in effect for the brush element.

1. One dimension brush

To answer your question for the specific example you linked to, we need to look at the brush object and scales objects. In that example, the brush is connected to the horizontal scale on the smaller, "context" chart (the x2 scale):

var x = d3.time.scale().range([0, width]),
    x2 = d3.time.scale().range([0, width]),
    y = d3.scale.linear().range([height, 0]),
    y2 = d3.scale.linear().range([height2, 0]);

var brush = d3.svg.brush()
    .x(x2)
    .on("brush", brushed);

brush's initialisation

The brush object created above only exists in the Javascript, not in the document. However, the object is also a function which can be called (similar to the axis functions) in order to create a series of rectangles which will respond to mouse events (these are invisible) and the one "extent" rectangle (which in this example is coloured gray with a white border).

context.append("g")
  .attr("class", "x brush")
  .call(brush)  //call the brush function, causing it to create the rectangles
.selectAll("rect") //select all the just-created rectangles
  .attr("y", -6)
  .attr("height", height2 + 7); //set their height

The default size of the invisible rectangles is based on the output range of the X and Y scales. Because this brush doesn't have a Y scale, the constant height and vertical position of the rectangles has to be set explicitly.

The initial size of the extent rectangle is based on the extent of the brush object (zero width and height by default). The height of that rectangle is also set in the above code.

brush interaction

When the user interacts with the brush on screen, the brush object captures those events and (1) updates the width of the "extent" rectangle, (2) calls the function which you associated with the "brush" event in the line .on("brush", brushed).

The brushed() function is:

function brushed() {
  x.domain(brush.empty() ? x2.domain() : brush.extent());
  focus.select(".area").attr("d", area);
  focus.select(".x.axis").call(xAxis);
}

The purpose of this brush is to scale the main chart, and this is done by setting the domain of the main chart's X-scale. If the brush has zero-width, brush.empty() returns true and the main chart X-domain is set to the full domain shown in the small chart.

However, if the brush has a valid width, the empty test returns false, and the domain is set to the results of brush.extent(). Because the brush is attached to a linear X scale, and no Y scale, the extent is returned in the form [xMin, xMax] (in the data numbers), which is exactly what is needed for setting the domain.

Extracting values from brush

If you needed to know the width in the data values, it is a simple matter of subtraction:

var extent = brush.extent(); //returns [xMin, xMax]
var width = extent[1] - extent[0]; //data-width = max - min

However, if you are drawing other elements on screen, you want to know the actual coordinates in the SVG, not just the data values. To do the conversion, you use the same thing you always use to convert from data to SVG coordinates: your scale function. Remembering to use the x2 scale that controls the small chart, not the zoomed-in scale of the main chart, that would look like:

var extent = brush.extent(); //returns [xMin, xMax]
var rangeExtent = [x2( extent[0] ), x2( extent[1] ) ]; //convert
var rangeWidth  = rangeExtent[1] - rangeExtent[0];

2. X and Y brush

To re-emphasize, this example is for a brush with one (horizontal / X) scale, which is a linear scale. If you were using both X and Y linear scales, you would need to separate out the X and Y extent values with code like this:

function brushed() {

  if (brush.empty()) {
      //either the height OR the width is empty
      x.domain( x2.domain() ); //reset X scale
      y.domain( y2.domain() ); //reset Y scale
  }

  var extent = brush.extent();
  x.domain( [extent[0][0] , extent[1][0] ] ); //min and max data X values
  y.domain( [extent[0][1] , extent[1][1] ] ); //min and max data Y values

  var rangeExtent = [
          [x2(extent[0][0]), y2(extent[0][1])],
          [x2(extent[1][0]), y2(extent[1][1])]
       ];
  var rangeWidth  = rangeExtent[1][0] - rangeExtent[0][0];
  var rangeHeight = rangeExtent[1][1] - rangeExtent[0][1];


  focus.select(".area").attr("d", area);
  focus.select(".x.axis").call(xAxis);
  focus.select(".y.axis").call(yAxis);
}

Extracting values from brush

If you want to know the coordinates of the top-left point of the rectangle, you'll also need to know whether your Y scale switches the minimum value from top to bottom.

Alternately, you could get the width, height, and top left coordinate from the on-screen "extent" rectangle, which the brush object modifies for you:

function brushed() {

  //use the brush object's values to set the data domains:
  if (brush.empty()) {
      //either the height OR the width is empty
      x.domain( x2.domain() ); //reset X scale
      y.domain( y2.domain() ); //reset Y scale
  }

  var extent = brush.extent();
  x.domain( [extent[0][0] , extent[1][0] ] ); //min and max data X values
  y.domain( [extent[0][1] , extent[1][1] ] ); //min and max data Y values

  //use the brush extent rectangle to get the SVG coordinates:
  var extentRect = d3.select("g.x.brush rect.extent");

  var rangeWidth  = extentRect.attr("width");
  var rangeHeight = extentRect.attr("height");
  var rangeLeft = extentRect.attr("x");
  var rangeTop = extentRect.attr("y");


  focus.select(".area").attr("d", area);
  focus.select(".x.axis").call(xAxis);
  focus.select(".y.axis").call(yAxis);
}

If you are using ordinal scales, it is more complicated for zooming, but less complicated for finding screen coordinates. This answer describes how to use a brush with an ordinal scale to zoom. However, since the values returned by .extent() are already in SVG coordinates, you would not have to use the scale itself to convert back if you want coordinates and width

References

The API page for the brush control has a set of thumbnail images at the top of the page; click on any of them to open up a working example. For more discussion, you might be interested in this tutorial with another good example of brushes in action.

like image 186
AmeliaBR Avatar answered Oct 13 '22 14:10

AmeliaBR