Michael Wooley's Homepage

Drawing With D3.js Part 1: Rectangles

In this post I am going to take a first step towards a sort of drawing app with the D3.js library. By the end of this post we’ll have a functioning-but-limited drawing app. In particular, we’ll be able to add rectangles to an svg canvas via a click-and-drag gesture. While there is a larger point to this little project, I’m not going to get into it right now. By the end of this series the purpose will come into focus.

Here’s what we’ll be working towards:

Click and drag within the box to add a rectangle. See the full sample code at this gist.

There are going to be two main steps today. First, we’ll set up an object that will serve as our “canvas” going forward. Then, we’ll add some methods to the canvas to add the ability to add rectangles.

The SVGCanvas Object

Let’s start out by making an object–call it SVGCanvas–that will both make a “drawing canvas” and provide a foundation for adding additional functionality at a later point in time. At the risk of introducing some ambiguity into the discussion, I will often speak of the “canvas” when discussing the object that will be drawn upon. However, be aware that this canvas will be SVG-based. I will not be using the HTML Canvas element at all in what follows.

To be precise, right now we’re going to make an object constructor. Here’s the starter code:

 1  function SVGCanvas(options) {
 2   /*
 3    *  An SVG-based drawing app.
 4    *  Input:
 5    *   - options: An object consisting of:
 6    *    - h: The height of the canvas (default: 250px).
 7    *    - w: The width of the canvas (default: 250px).
 8    *    - addTo: CSS Selector for element on which to add canvas (default: 'body').
 9    *    - addBorderRect: (bool) Add a border around the canvas (default: true).
10    *  Returns: An SVG object contained in the `addTo` DOM element.
11   */
12   var self = this;
13   // Define the global SVG options
14   this.options = options || {};
15   this.options.h = options.h || 250;
16   this.options.w = options.w || 250;
17   this.options.addTo = options.addTo || 'body';
18   this.options.addBorderRect = options.addBorderRect || true;
19 
20   // Canvas
21   //// Make the main container SVG
22   this.svg = d3.select(this.options.addTo)
23     .append('svg')
24     .attr('height', this.options.h)
25     .attr('width', this.options.w)
26     .attr('class', 'display-svg');
27   //// Add border if requested
28   if (this.options.addBorderRect) {
29     this.svg.append('rect')
30       .attr('height', this.options.h)
31       .attr('width', this.options.w)
32       .attr('stroke', 'black')
33       .attr('stroke-width', 4)
34       .attr('opacity', 0.25)
35       .attr('fill-opacity', 0.0)
36       .attr('class', 'border-rect');
37   }
38 
39   // More to come...
40 }

A lot of this is important-but-mundane boilerplate. The constructor takes as an argument an options object, which specifies different options related to the dimensions and placement of the object in the DOM. A final option specifies whether a border should be added to the canvas, which helps users see where it is valid to draw. If we just add an <svg> element to the DOM and nothing else there will just be blank space. That takes care of lines 1-18.

The code beginning in line 22 creates the canvas in the form of an <svg> element. This is also the first time that we’ve encountered D3 in the code.


Aside: Basic D3.js

I will be using D3 v4.0. The API documentation can be found here. Here’s a quick run-down of the basic pattern that we follow when creating elements with d3:

var div = 			// Define a variable in javascript called "div"
    d3.select('div.test')	// Select the first div element with class "test".
				// 	This element must already exist in the DOM.
    .append('div')		// Add/"append" a <div> element to the selected element
				// 	This element is nested in the selected div.
    .attr('class', 'container') // Add an attribute to the element.
				// 	class="container"
    .attr('title', 'Test Div!') // Add another attribute: chain them on.
				// 	title="Test Div!"
    .style('float', 'left')	// Add inline styling to the element.
				// 	style="float: left"
    .style('position', 'fixed') // You can also chain styling.
    ;				// 	style="float: left; position: fixed"

div.append('svg');		// Once we define a variable for the element we
				// 	can skip the d3.select() step.
				// Here we nest an <svg> element in the
				//	newly-created div. We could add more
				//	attributes and styling as above.

Suppose that our original HTML looked like this:

<div class="test"></div>

Once we run the above script it will look like this:

<div class="test">
  <div class="container" title="Test Div!" style="float: left; position: fixed;">
    <svg></svg>
  </div>
</div>

In short, D3.js provides an easy way of altering the DOM. There is a lot more to D3.js than this. However, this simple select-append pattern will turn out to be quite powerful.


Our canvas: a black-ish square that doesn’t do anything (yet).

Returning to the SVGCanvas constructor codeblock we can see that an svg element is appended to the DOM in lines 22-26. We define the parent element, height, and width using the options that were just defined. Lines 28-36 then append an svg <rect> element to the SVG that was just created. This is our border.

Okay, that’s it for the canvas. At this point we just have a black-ish square.

Adding Rectangles

Now that we have our canvas in place we can start to add interactive elements. The first thing that we’re going to do is make it possible to create a rectangle via a click-and-drag gesture.

To be specific, we want to implement the following behavior:

  1. When the user clicks (i.e. mousedown/start) on the svg element a <rect>-angle element is created in the svg.
  2. While the user is still holding down the mouse (i.e. drag): Moving the mouse causes the rectangle to expand and contract. One corner of the rectangle remains at the point of the initial click while the opposite diagonal corner follows the mouse.
  3. When the user releases the click (i.e. mouseup/end) the rectangle remains in place.

Luckily, D3 makes it fairly easy to do this sort of thing. Here is the plan: First, we’re going to create three methods corresponding to the three steps below. Then, we’re going to add a drag event listener to the svg element that calls each of the three methods whenever the computer registers a click-and-drag gesture.

I’m going to work backwards through these two steps.

Drag Event Listener

We’re going to alter the constructor for the SVGCanvas element to add event listeners. This is another area where D3 excels. Here’s a snippet of the constructor code:

function SVGCanvas(options) {
  // ...Previous code...

  // Rectangles
  //// Current Rectangle
  this.Rect = {'r': null, 'x0': null, 'y0': null};
  //// Collection of all shapes
  this.Shapes = [];

  // Actions/Listeners
  //// Methods for adding rectangles
  this.makeAddRect();

  // On drag: call addRect methods
  this.svg.call(
    d3.drag()
    .on('start', this.addRect.start)
    .on('drag', this.addRect.drag)
    .on('end', this.addRect.end)
  );
}

In the first few lines I’ve added some code that will make more sense in a moment. For now, though, I want to focus on the line beginning with this.svg.call(...). Let’s walk through this.

  • this.svg.call() invokes a callback function on the svg element that we just defined previously.
  • d3.drag() creates a new drag behavior. Since it is nested in the this.svg.call(), this behvior will be activated whenever the this.svg is dragged.
  • .on('start', this.addRect.start) specifies what should happen when we, well, start to drag the mouse on this.svg. In particular, it says to invoke a function called this.addRect.start, which we haven’t defined yet. Similarly, .on('drag', this.addRect.drag) and .on('end', this.addRect.end) specify what should happen as the mouse is dragged and at the end of the drag (i.e. when the mouse is released).

Adding the addRect Methods

Now that we know how to add a dragging behavior to the canvas, we need to define the methods that are called at the start, drag, and end of the drag. To do this I’m going to define a prototype–makeAddRect–which will append the methods to our SVGCanvas object.

Here is the code:

 1 SVGCanvas.prototype.mouseOffset = function() {
 2   // Get the current location of mouse along with other info (to be added to later)
 3   var m = d3.event;
 4   return m;
 5 }
 6 
 7 SVGCanvas.prototype.makeAddRect = function() {
 8   // Methods for adding rectangles to the svg.
 9   var self = this;
10 
11   start = function() {
12     //Add a rectangle
13     // 1. Get mouse location in SVG
14     var m = self.mouseOffset();
15     self.Rect.x0 = m.x;
16     self.Rect.y0 = m.y;
17     // 2. Make a rectangle
18     self.Rect.r = self.svg //self.zoomG
19       .append('g')
20       .append('rect') // An SVG `rect` element
21       .attr('x', self.Rect.x0) // Position at mouse location
22       .attr('y', self.Rect.y0)
23       .attr('width', 1) // Make it tiny
24       .attr('height', 1)
25       .attr('class', 'rect-main') // Assign a class for formatting purposes
26     ;
27   }
28 
29   drag = function() {
30     // What to do when mouse is dragged
31     // 1. Get the new mouse position
32     var m = self.mouseOffset();
33     // 2. Update the attributes of the rectangle
34     self.Rect.r.attr('x', Math.min(self.Rect.x0, m.x))
35       .attr('y', Math.min(self.Rect.y0, m.y))
36       .attr('width', Math.abs(self.Rect.x0 - m.x))
37       .attr('height', Math.abs(self.Rect.y0 - m.y));
38   }
39 
40   end = function() {
41     // What to do on mouseup
42     self.Shapes.push(self.Rect);
43   }
44 
45   self.addRect = {start: start, drag: drag, end: end};
46 }

In short, .makeAddRect defines three functions then adds these functions to the SVGCanvas object as an object/dictionary.

What to do at the beginning of a drag: addRect.start

When we begin dragging we’re going to want to:

  1. Record the initial position of the mouse.
  2. Add a <rect> element within the svg.

This first step is carried out in lines 14-16. Line 14 calls a method called mouseOffset, which returns the current (i.e. initial) position of the mouse. At this point mouseOffset is overkill. However, it will come in handy once we begin zoom and pan the canvas. We then record these coordinates in the self.Rect.x0 and self.Rect.y0. Recall that self.Rect was defined in the constructor function. We will use self.Rect to share variables between addRect.start, addRect.drag, and addRect.end (and, eventually, other methods).

Next, we make a tiny rectangle (line 18). We assign this object to self.Rect.r so that we can modify it later. Notice that, before we append the <rect> element, we append a <g> (group) element to the svg. This nests the <rect> in the <g> element:

<svg>
  <g>
    <rect></rect>
  </g>
</svg>

For what we’re doing right now, this isn’t necessary. However, it will come in handy later.

What to do while dragging: addRect.drag

Each time a drag event is registered we’re going to update the dimensions of the <rect> that we just made to reflect the change in the mouse position. As with addRect.start we begin by getting the current mouse position.

We then modify the attributes the <rect> element that we just created. Depending on where the mouse was dragged, the initial position could be any one of the four corners of the rectangle. We take account of this by comparing the current mouse position with the initial mouse position, which we saved in self.Rect.x0 and self.Rect.y0.

What to do when the drag is complete: addRect.end

At the moment we don’t have to do anything at the completion of the drag event. However, we will want to do some things eventually.

For now, we add the current self.Rect to the self.Shapes array. By doing this we save the information about the rectangle for later. This could be useful because–once a new drag event is initiated–the information in self.Rect will be overwritten with information about the newly-created rectangle.

A Note on Context: var self = this;

Now I want to discuss line 9. What does this line do? It defines a variable, self, which is equal to this. Here, this is the SVGCanvas object. Thus, we can access all of the properties of the SVGCanvas object via references to self. Indeed, notice that in each of the three methods just defined we always used self rather than this.

Why do we need to do this? Go back to the line in the constructor where we defined the drag behavior:

this.svg.call(
  d3.drag()
  .on('start', this.addRect.start)
  .on('drag', this.addRect.drag)
  .on('end', this.addRect.end)
);

In this snippet, “this” refers to the SVGCanvas object. This is because the line was executed in the context of the SVGCanvas constructor. However, when this.addRect.start is called the context will be different. In particular, this.svg.call(...) defines a callback on the svg element, which means that the context is the svg element. Thus, if we were to reference this within the addRect.start method, we would be referring to the svg element rather than the SVGCanvas object. Our attempts to reference this.Rect.x0 (for example) would result in an error because the svg object doesn’t have a Rect property.

Demo: mutable objects in javascript.

By defining var self = this; at the outset of .makeAddRect, we set down a permanent reference to the SVGCanvas object. Of course, this can only work if self is mutable, which it is. That is, self will mirror changes in this.

To close this section I will note that I’m not a huge fan of this setup. My main complaint is that we need to be sure to call this.makeAddRect(); in the constructor function in the correct place. If anyone has a thought about how to smooth this out I’m all ears.

Conclusion

Okay, that’s it. The full code for this post can be found at this gist. Obviously, this is a fairly rudimentary step. Going forward we’re going to want to think about:

  • Zooming and panning the canvas.
  • Modifying the rectangles that we’ve added:
    • Resizing
    • Repositioning
    • Deleting
  • Adding more information to the canvas:
    • Different types of rectangles.
    • Extracting information from images.
    • Downloading.

I will take up these threads in future posts.