Brushing in d3.js





This is document gives a few insights on how to add brushing with d3.js. It is composed by several interactive examples, allowing to play with the code to understand better how it works.

Creating a brush area with d3.brush().



Steps:

  • This example just show how to add a brush area with d3.brush.

  • Note that this brush area does nothing yet.

  • The .extent() property specify the area where it is possible to brush: top left and bottom right corners of the rectangle.
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<svg width=400 height=400 id="dataviz_brushing"></svg>

<script>

// Draw a circle
var myCircle = d3.select("#dataviz_brushing")
  .append("svg")
  .append("circle")
    .attr("cx", 200)
    .attr("cy", 200)
    .attr("r", 40)
    .attr("fill", "#69a3b2")

// Add brushing
d3.select("#dataviz_brushing")
      .call( d3.brush()                     // Add the brush feature using the d3.brush function
        .extent( [ [0,0], [400,400] ] )       // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
      )

</script>

One dimensional brush with d3.brushX() and d3.brushY().



Steps:

  • Just call d3.brushX instead of d3.brush and you get a 1 dimensional brush.

  • Note that here the zone is limited: only brushing around the circle works.
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<svg width=400 height=400 id="dataviz_brushing1D"></svg>

<script>

// Draw a circle
var myCircle = d3.select("#dataviz_brushing1D")
  .append("svg")
  .append("circle")
    .attr("cx", 200)
    .attr("cy", 200)
    .attr("r", 40)
    .attr("fill", "#69a3b2")

// Add brushing
d3.select("#dataviz_brushing1D")
      .call( d3.brushX()                     // Add the brush feature using the d3.brush function
        .extent( [ [0,100], [400,300] ] )       // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
      )

</script>

Trigger a change with .on("start end", ...)



Steps:

  • Add .on("start end", ...) when the brush is created. This will trigger the function updateChart each time a new brush is made.

  • This function starts by getting the selection coordinates thanks to d3.event.selection

  • Then it checks if the circles is in it and eventually changes its color.
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<svg width=300 height=300 id="dataviz_brushChange"></svg>

<script>

// Draw a circle
var myCircle = d3.select("#dataviz_brushChange")
  .append("g")
  .append("circle")
    .attr("cx", 150)
    .attr("cy", 150)
    .attr("r", 40)
    .attr("fill", "#69a3b2")

// Add brushing
d3.select("#dataviz_brushChange")
      .call( d3.brush()                 // Add the brush feature using the d3.brush function
        .extent( [ [0,0], [300,400] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
        .on("start end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
      )

// Function that is triggered when brushing is performed
function updateChart() {

  // Get the selection coordinate
  extent = d3.event.selection   // looks like [ [12,11], [132,178]]

  // Is the circle in the selection?
  isBrushed = extent[0][0] <= myCircle.attr("cx") && extent[1][0] >= myCircle.attr("cx") && // Check X coordinate
              extent[0][1] <= myCircle.attr("cy") && extent[1][1] >= myCircle.attr("cy")  // And Y coordinate

  // Circle is green if in the selection, pink otherwise
  if (isBrushed){
    myCircle.transition().duration(200).style("fill", "green")
  }else{
    myCircle.transition().duration(200).style("fill", "pink")
  }
}

</script>

Use CSS to highlight selection with .classed



Steps:

  • Very close from previous example, but here the style is controled using CSS.

  • A selected class is added to the circle if it is in the selection.

  • The .classed function is very handy since it automatically removes the class if elements are not in the selection anymore.
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<svg width=300 height=300 id="dataviz_brushCSS"></svg>

<!-- style of selected circles -->
<style>
.selected {
  fill: red;
  opacity: 1
}
</style>

<script>

// Draw a circle
var myCircle = d3.select("#dataviz_brushCSS")
  .append("g")
  .append("circle")
    .attr("cx", 150)
    .attr("cy", 150)
    .attr("r", 40)
    .attr("fill", "#69a3b2")

// Add brushing
d3.select("#dataviz_brushCSS")
      .call( d3.brush()                 // Add the brush feature using the d3.brush function
        .extent( [ [0,0], [300,400] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
        .on("start brush", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
      )

// Function that is triggered when brushing is performed
function updateChart() {

  // Get the selection coordinate
  extent = d3.event.selection   // looks like [ [12,11], [132,178]]

  // Is the circle in the selection?
  isBrushed = extent[0][0] <= myCircle.attr("cx") && extent[1][0] >= myCircle.attr("cx") && // Check X coordinate
              extent[0][1] <= myCircle.attr("cy") && extent[1][1] >= myCircle.attr("cy")  // And Y coordinate

  // Circle is green if in the selection, pink otherwise
  myCircle.classed("selected", isBrushed)
}

</script>

Application on a real scatterplot



Steps:

  • Exactly the same idea than before, with a real chart

  • Note that addin !important in the css part is necessary to overwrite a feature that specified in the javascript part.

<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<div id="dataviz_brushScatter"></div>

<!-- style of selected circles -->
<style>
.selected {
  opacity: 1 !important;
  stroke: black;
  stroke-width: 1px;
}
</style>

<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
    width = 460 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
var svg = d3.select("#dataviz_brushScatter")
  .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {

  // Add X axis
  var x = d3.scaleLinear()
    .domain([4, 8])
    .range([ 0, width ]);
  svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x));

  // Add Y axis
  var y = d3.scaleLinear()
    .domain([0, 9])
    .range([ height, 0]);
  svg.append("g")
    .call(d3.axisLeft(y));

  // Color scale: give me a specie name, I return a color
  var color = d3.scaleOrdinal()
    .domain(["setosa", "versicolor", "virginica" ])
    .range([ "#440154ff", "#21908dff", "#fde725ff"])

  // Add dots
  var myCircle = svg.append('g')
    .selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
      .attr("cx", function (d) { return x(d.Sepal_Length); } )
      .attr("cy", function (d) { return y(d.Petal_Length); } )
      .attr("r", 8)
      .style("fill", function (d) { return color(d.Species) } )
      .style("opacity", 0.5)

  // Add brushing
  svg
    .call( d3.brush()                 // Add the brush feature using the d3.brush function
      .extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
      .on("start brush", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
    )

  // Function that is triggered when brushing is performed
  function updateChart() {
    extent = d3.event.selection
    myCircle.classed("selected", function(d){ return isBrushed(extent, x(d.Sepal_Length), y(d.Petal_Length) ) } )
  }

  // A function that return TRUE or FALSE according if a dot is in the selection or not
  function isBrushed(brush_coords, cx, cy) {
       var x0 = brush_coords[0][0],
           x1 = brush_coords[1][0],
           y0 = brush_coords[0][1],
           y1 = brush_coords[1][1];
      return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1;    // This return TRUE or FALSE depending on if the points is in the selected area
  }

})

</script>

Brushing for zooming



Notes:

  • Select a zone to zoom on it (X axis only). Double click to unzoom.

  • A clipPath is used to avoid displaying the circle outside the chart area.

  • all(brush.move, null) is used to dismiss the grey brushing area once the selection has been done.

  • But this leads to synchronization problems: the brush boundaries are erased as soon as the selection is done --> It is necessary to wait a little bit to have time to update axis and circles
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the circle will take place -->
<div id="dataviz_brushZoom"></div>

<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
    width = 460 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
var Svg = d3.select("#dataviz_brushZoom")
  .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/iris.csv", function(data) {

  // Add X axis
  var x = d3.scaleLinear()
    .domain([4, 8])
    .range([ 0, width ]);
  var xAxis = Svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x));

  // Add Y axis
  var y = d3.scaleLinear()
    .domain([0, 9])
    .range([ height, 0]);
  Svg.append("g")
    .call(d3.axisLeft(y));

  // Add a clipPath: everything out of this area won't be drawn.
  var clip = Svg.append("defs").append("svg:clipPath")
      .attr("id", "clip")
      .append("svg:rect")
      .attr("width", width )
      .attr("height", height )
      .attr("x", 0)
      .attr("y", 0);

  // Color scale: give me a specie name, I return a color
  var color = d3.scaleOrdinal()
    .domain(["setosa", "versicolor", "virginica" ])
    .range([ "#440154ff", "#21908dff", "#fde725ff"])

  // Add brushing
  var brush = d3.brushX()                 // Add the brush feature using the d3.brush function
      .extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
      .on("end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function

  // Create the scatter variable: where both the circles and the brush take place
  var scatter = Svg.append('g')
    .attr("clip-path", "url(#clip)")

  // Add circles
  scatter
    .selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
      .attr("cx", function (d) { return x(d.Sepal_Length); } )
      .attr("cy", function (d) { return y(d.Petal_Length); } )
      .attr("r", 8)
      .style("fill", function (d) { return color(d.Species) } )
      .style("opacity", 0.5)

  // Add the brushing
  scatter
    .append("g")
      .attr("class", "brush")
      .call(brush);

  // A function that set idleTimeOut to null
  var idleTimeout
  function idled() { idleTimeout = null; }

  // A function that update the chart for given boundaries
  function updateChart() {

    extent = d3.event.selection

    // If no selection, back to initial coordinate. Otherwise, update X axis domain
    if(!extent){
      if (!idleTimeout) return idleTimeout = setTimeout(idled, 350); // This allows to wait a little bit
      x.domain([ 4,8])
    }else{
      x.domain([ x.invert(extent[0]), x.invert(extent[1]) ])
      scatter.select(".brush").call(brush.move, null) // This remove the grey brush area as soon as the selection has been done
    }

    // Update axis and circle position
    xAxis.transition().duration(1000).call(d3.axisBottom(x))
    scatter
      .selectAll("circle")
      .transition().duration(1000)
      .attr("cx", function (d) { return x(d.Sepal_Length); } )
      .attr("cy", function (d) { return y(d.Petal_Length); } )

    }



})

</script>