DEV Community

Chelsea Rosic
Chelsea Rosic

Posted on

Selecting by Dragging Using Brushes in D3 and SVG

In this article I will cover how to write code that allows users to pan and to select SVG circles with D3 brushes.

Since creating brushes in D3 and panning both require users to click and then drag, we need to create a button to toggle between two modes: one mode which allows users to select elements by dragging/creating brushes, and another mode which allows users to pan on the screen.

Codepen of Final Product

This article will be explaining the code from the codepen below in further detail.

Sometimes the codepen output is a bit glitchy when embedded in this article or in editing view (i.e. it won't let me select some of the circles) so I suggest either changing the zoom to 0.5x or lower at the bottom-center of this embedded codepen or viewing it in full page view:

Step 1: HTML and CSS

This part is pretty straightforward, we need to create and style a button that allows users to switch between the two modes, and a div that will hold our svg elements.

When the button is clicked it calls on a javascript function called onButtonClick() which will be explained in further detail in step 3.

HTML:

<button id="button" onclick="onButtonClick()" >Select By Dragging</button>

<div id="svg_div"> </div>

<script src="https://d3js.org/d3.v4.min.js"></script>

I styled the webpage so the button is in the top-left corner of the screen and the position of the button is fixed so when users zoom in the button will still be at the top-left of the screen.

CSS:

button {
  position: fixed;
  top: 0;
  background-color:blue;
  border: none;
  color: white;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 20px;
  cursor: pointer;
}

div {
  position: fixed;
  top: 5%;
  height: 95%;
  width: 100%;
  z-index:-1;
}

Step 2: Setting up webpage with Panning Mode

First we will create a SVG element called svg inside of our HTML div named svg_div. The SVG element will be a container for SVG circles and brush elements. We are setting the width and height to 100% so the SVG element will take up the whole div.

The default for our webpage will allow users to zoom and pan on the SVG element by calling the zoom function/variable. When zoom is called, it allows users to zoom and pan around the SVG element. It does this by transforming the SVG element using the information from the d3 event. More information on SVG transformations can be found here.

var selected_circles = []; // holds the IDs for the circles that are currently selected 

// Zoom function for enabling panning and zooming across the canvas
const zoom = d3.zoom().on("zoom", function () {
          svg.attr("transform", d3.event.transform);
});

// create svg element to hold circle/other elements:
var svg = d3.select("#svg_div")
            .append("svg")
            .attr("id", "parent_svg_elem")
            .attr("width", "100%")
            .attr("height", "100%")
            .call(zoom)
            .append("g");

We can add SVG circles and other shapes to the SVG element that we just created. In this case I added 11 circles in random locations.

// create a bunch of random circles 
for (var i = 1; i < 11; i++) {
  var x = (Math.random() * 970) + 10;
  var y = (Math.random() * 770) + 10;
  svg.append('circle')
    .attr("id", i)
    .attr('cx', x)
    .attr('cy', y)
    .attr('r', 10)
    .attr('stroke50', 'black')
    .attr('fill', 'blue');  
}

Step 3: Switching Between Selecting by Dragging and Panning Mode

Now that we can zoom and pan, we need to create and option to allow users to brush on our SVG element.

Basically this can be boiled down to adding a SVG brush (an element which allows users to create brush rectangles on its surface) to our SVG element when the user wants to select with brushes and removing the brush element when the user wants to pan on the screen.

We use the boundary of our SVG element as the boundary for our brush element since we want to allow users to brush on any part of the SVG element. Brush supports 3 kinds of events: end, start, and brush. When the user is done drawing a brush rectangle, we call the brushend function to select circles within the brush. This function will be discussed in the next section.

function switchButtonColor() {
  const button = document.getElementById('button');
  if (button.style.backgroundColor === "blue" || button.style.backgroundColor === "" ) {
    button.style.backgroundColor = "red";
  }
  else {
    button.style.backgroundColor = "blue";
  }
}


function onButtonClick() {
  switchButtonColor();  

  // add brush element because it does not exist 
  if ( d3.selectAll("#brush_id").empty())  {

  // create a variable that allowing users to implement brushing 
  // set it to call the brushend function whenever a new brush rectangle is created
  var brush = d3.brush().on("end", () => brushend(svg));

  const bbox = document.getElementById("svg_div").getBoundingClientRect();

  svg
    .classed('brush', true)
    .append("g")
    .attr("id", "brush_id")
    .datum({
      extent: [[bbox.x, bbox.y], [bbox.x+bbox.width, bbox.y+bbox.height]],
    })
    .call(brush);
  }  

  // if brush is currently activated then remove it
  else  {
    d3.selectAll("#brush_id").remove();
  } 
}

Step 4: Selecting Elements Within Brush/Rectangle

When a user creates a new brush rectangle, we extract the coordinates of the brush rectangle from the event, iterate through all of the circles in the webpage, and see if any of the circles are within the rectangle. If a circle is within the rectangle then will call toggleSelection on its ID. This function will select or unselect the circle by removing or adding the circle to the selected_circles array, and will also change the circle's colour (red if the circle is selected, blue if the circle is not selected).

function toggleSelection(id) {
  const circle = document.getElementById(id);

  if ( !selected_circles.includes(id) ) {
    selected_circles.push(id);
    circle.setAttribute("fill", "red");
  }

  else {
    // remove element from selected circles array 
    selected_circles.splice(selected_circles.indexOf(id), 1);
    circle.setAttribute("fill", "blue");
  }
}

function brushend(svg) {

  // coordinates of the brush
  const selectionCordinates = d3.event.selection;

  // selection coordinates will be null if user clicks while in brush mode
  if (selectionCordinates !== null) {

    let targetX1 = selectionCordinates[0][0];
    let targetY1 = selectionCordinates[0][1];
    let targetX2 = selectionCordinates[1][0];
    let targetY2 = selectionCordinates[1][1];

    // get an array of all circles 
    const circles = d3.selectAll("circle").nodes();

    circles.forEach(element => {  
      curr_x = element.cx.baseVal.value; 
      curr_y = element.cy.baseVal.value; 

      // see if node is in the brush rectangle
      if (curr_x >= targetX1 && curr_x <= targetX2 && 
          curr_y >= targetY1 && curr_y <= targetY2) {
            toggleSelection(element.id);
      }
    });
  }
}

Conclusion

Hopefully this article was helpful! This is my first blog post here! I'm really excited to be a part of the DEV community. Let me know if you liked this article and found it helpful, and if there are ways I can improve. Also, are there any other D3, SVG, spatial data, or other topics that you would like me to cover? Let me know in the discussion below!

Top comments (0)