Chapter 05Paths

In this section we discuss d3.path(), a utility for creating strings that can be used to define paths in svgs.

D3.path can be used by itself to define the value of the d (path definition) attribute for a path element in an svg, as we show in this section, but is also used by all of the other shapes in this chapter to transform various input into path defininitions.

SVG Paths

SVG’s can contain various elements to help create a visualization. Of these elements, lines and rects are useful in creating some common data visualization patterns such as line graphs and bar graphs. When requiring complex shapes we rely on the path element.

Paths can be appended to svgs like lines and rects and have similar attributes, such as stroke and fill. Paths, however, have a d attribute that defines its shape and position.

The d attribute contains a string that specifies commands and numbers which define command parameters. Multiple commands can be chained together in a single string and are read from left to right.

For example, to create a path that starts at the point (25,25), draws a line to (75,25), then draws a line to (75,75) and finally closes the path we can use the following path definition.

"M 25 25 L 75 25 L 75 75 Z"
<script>
d3.select("#demoSvgPath")
  .append("path")
  .attr("d", "M 25 25 L 75 25 L 75 75 Z")
  .attr("stroke", "black")
  .attr("fill", "red");
</script>
<svg id="demoSvgPath" width="100" height="100"></svg>
Figure 1. Triangle created by manually specifying the path description.

The Mozilla documentation for all of the available SVG path commands can be found here.

D3.path

The d3.path() method is convenient for defining paths, allowing us to avoid manually writing strings for the d attribute of a path. The d3.path method returns a path serializer that implements the methods in the CanvasPathMethods interface. Below we provide links to the D3.js documentation for each method along with links to their equivalent context method and their equivalent SVG command. In the sections below we describe each method and provide examples of their usage.

Path Method Context Method SVG Command
moveTo(x, y) moveTo(x, y) M
lineTo(x, y) lineTo(x, y) L
closePath() closePath() Z, z
quadraticCurveTo(cpx, cpy, x, y) quadraticCurveTo(cpx, cpy, x, y) Q
bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y) bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y) C
arcTo(x1, y1, x2, y2, radius) arcTo(x1, y1, x2, y2, radius) A
arc(x, y, radius, start, end[, anticlockwise]) arc(x, y, radius, start, end, anticlockwise) A
rect(x, y, w, h) rect(x, y, w, h)
toString()

Setting an SVG’s Path

In order to create a triangle, similar to the one above, using the methods describe above, we first have to create a path serializer by calling d3.path:

var path = d3.path();

We can then call the path methods on the seriarlizer object.

path.moveTo(25,25);
path.lineTo(75,25);
path.lineTo(75,75);
path.closePath();

We then retrive the SVG path definition string using path.toString. This string is used as the value for an SVG path's d attribute. As you can see from the console output in the example, the path is similar to the path used in the example above. The only difference is the delimeter used between the command parameters. When defining the path manually we used spaces, whereas path.toString uses commas.

<script>
var path = d3.path()
path.moveTo(25,25)
path.lineTo(75,25)
path.lineTo(75,75)
path.closePath();
  
console.log(path.toString());
    
d3.select("#demoPath0")
  .append("path")
  .attr("d", path.toString())
  .attr("stroke", "black")
  .attr("fill", "red");
</script>

<svg id="demoPath0" width="100" height="100"></svg>
Figure 2. Triangle created using d3.path.

When setting the d attribute for a path element, we can also simply pass the path serializer as the second argument of selection.attr as shown in the example below.

d3.select("#demoPath1")
  .append("path")
  .attr("d", path)
  .attr("stroke", "black")
  .attr("fill", "red");

Path.moveTo, Path.LineTo, and Path.close

In the example below, we use an array to define a set of six points and use a single d3.path serializer along with path.moveTo, path.lineTo, and path.closePath to create two separate triangles (left image).

We are able to create two separately filled shapes using a single path serializer due to the serializer’s ability to maintain subpaths withing a path. A subpath is a portion of a path that has a beginning and an end. When we call path.moveTo or path.closePath we both end the current subpath and begin a new subpath.

As can be seen, the top triangle is not closed and the bottom triangle is closed.

<script>
points = [[50,50],[150,125],[200,25],[200,150],[100,250],[250,275]];

var path = d3.path();
	path.moveTo(points[0][0], points[0][1]); //(50,50)
    path.lineTo(points[1][0], points[1][1]); //(150,125)
    path.lineTo(points[2][0], points[2][1]); //(200,25)
    
    path.moveTo(points[3][0], points[3][1]); //(200,150)
    path.lineTo(points[4][0], points[4][1]); //(100,250)
    path.lineTo(points[5][0], points[5][1]); //(250,275)
    path.closePath();
    
d3.select("#demoPath2")
	.append("path")
    .attr("d", path)
    .attr("stroke", "black")
    .attr("fill", "red");
    
d3.select("#demoPath2Outline")
	.append("path")
    .attr("d", path)
    .attr("stroke", "black")
    .attr("fill", "red");
    
addPathOutline(points, d3.select("#demoPath2Outline"));
</script>

<svg id="demoPath2" width= "300" height="300"></svg>
<svg id="demoPath2Outline" width= "300" height="300">
</svg>
Figure 3. Pairs of triangles created using path.moveTo, path.lineTo, and path.closePath.

Curves

Below we draw Bézier curves using path.quadraticCurveTo (top) and path.bezierCurveTo (bottom).

The control points for the methods are depicted by the red dots that are positioned off of the curves.

<script>

// path.quadraticCurveTo example

var qCP = [25, 80];

var qPath = d3.path();
qPath.moveTo(10,10);
qPath.quadraticCurveTo(qCP[0],qCP[1], 190, 10);

d3.select("#demoCurve")
  .append("path")
  .attr("d", qPath)
  .attr("stroke", "black")
  .attr("fill", "none");

// path.bezierCurveTo example

var bCP1 = [25, 120];
var bCP2 = [175, 190];

var bPath = d3.path();
bPath.moveTo(10,190);
bPath.bezierCurveTo(bCP1[0],bCP1[1], bCP2[0], bCP2[1], 190, 120);

d3.select("#demoCurveOutline")
  .append("path")
  .attr("d", qPath)
  .attr("stroke", "black")
  .attr("fill", "none");
    
// Draw control points and lines to control points

d3.select("#demoCurve")
  .append("path")
  .attr("d", bPath)
  .attr("stroke", "black")
  .attr("fill", "none");

d3.select("#demoCurveOutline")
  .append("path")
  .attr("d", bPath)
  .attr("stroke", "black")
  .attr("fill", "none");
    
addCurveOutline(qCP, bCP1, bCP2, d3.select("#demoCurveOutline"));

</script>

<svg id="demoCurve" width="200" height="200"></svg>
<svg id="demoCurveOutline" width="200" height="200"></svg>
Figure 4. Curves created using path.quadraticCurveTo (top) and path.bezierCurveTo (bottom).

Arcs

The example below uses path.arc to draw the lower right quadrant of a circle.

<script>
var center = [100, 100];
var radius = 75;
var startAngle = 0;
var endAngle = Math.PI / 2;

var arcPath = d3.path();
	arcPath.moveTo(center[0], center[1]);
    arcPath.arc(center[0], center[1], radius, startAngle, endAngle)
    arcPath.closePath();
    
d3.select("#demoArc")
	.append("path")
    .attr("d", arcPath)
    .attr("stroke", "black")
    .attr("fill", "red");
    
d3.select("#demoArcOutline")
    .append("path")
    .attr("d", arcPath)
    .attr("stroke", "black")
    .attr("fill", "red");

addArcOutline(center, radius, d3.select("#demoArcOutline"));

</script>
<svg id="demoArc" width="200" height="200"></svg>
<svg id="demoArcOutline" width="200" height="200"></svg>
Figure 5. Quadrant shape created using path.arc.

Below we draw a shape using path.arcTo.

In our example, the point at the bottom right corner of the shape (160,190) is the current point from which the initial line and arc are drawn. We then draw a line from the end of the arc to the point at (185,25), and close the path which renders a line from (185,25) to our starting point (160,190).

<script>
var points = [[160,190],[10,100],[185,25]];
var radius = 30;

var arcToPath = d3.path();
arcToPath.moveTo(points[0][0], points[0][1]);
arcToPath.arcTo(points[1][0], points[1][1], points[2][0], points[2][1], radius)
arcToPath.lineTo(points[2][0], points[2][1])
arcToPath.closePath();

d3.select("#demoArcTo")
  .append("path")
  .attr("d", arcToPath)
  .attr("stroke", "black")
  .attr("fill", "red");
    
d3.select("#demoArcToOutline")
  .append("path")
  .attr("d", arcToPath)
  .attr("stroke", "black")
  .attr("fill", "red");
    
addArcToOutline(points[0], points[1], points[2], radius, d3.select("#demoArcToOutline"));

</script>
<svg id="demoArcTo" width="200" height="200"></svg>
<svg id="demoArcToOutline" width="200" height="200"></svg>
Figure 6. Demonstration of path.arcTo.

Rectangles

Below are examples of rectangles drawn using path.rect.

<script>
var origin = [25,25];
var width = 150;
var height = 150;

var path = d3.path();
    path.rect(origin[0], origin[1], width, height);

d3.select("#demoRect")
    .append("path")
    .attr("d", path)
    .attr("fill", "red")
    .attr("stroke", "black");

d3.select("#demoRectOutline")
    .append("path")
    .attr("d", path)
    .attr("fill", "red")
    .attr("stroke", "black");
    
addRectOutline(origin, width, height, d3.select("#demoRectOutline"));
</script>
<svg id="demoRect" width="200" height="200"></svg>
<svg id="demoRectOutline" width="200" height="200"></svg>
Figure 7. Rectangles created using path.rect.

Canvases

Since the d3.path serializer has the same methods as in the CanvasPathMethods interface, then any function that only uses the methods in the CanvasPathMethods interface and that takes a canvas object as an argument, can be passed a path serializer.

Below we render a triangle twice, using the method named draw. We first pass d3.path to the draw method to draw a triangle in an SVG path element and then draw a triangle in a canvas element by passing the canvas' 2D context to the draw method.

<script>
function draw(context){
  context.fillStyle = "red";
  context.moveTo(25,25);
  context.lineTo(75,25);
  context.lineTo(75,75);
  context.closePath();
  return context;
}

d3.select("#demoFunctionSVG")
  .append("path")
  .attr("d", draw(d3.path()))
  .attr("stroke", "black")
  .attr("fill", "red");
    
var context = d3.select("#demoFunctionCanvas").node().getContext("2d");
draw(context).fill();
context.stroke();
</script>

<svg id="demoFunctionSVG" width=100 height=100></svg>
<canvas id="demoFunctionCanvas" width=100 height=100></canvas>
Figure 8. Two triangles. One drawn on an SVG and the other drawn on a canvas.