Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso

Perpendicular Bisectors of a Triangle With d3.js

Following up from my last post on how to draw the altitude of a side of a triangle through a vertex, I wanted to draw the 3 perpendicular bisectors of a triangle and the circumcircle of the triangle.

Let us get some definitions for these terms, the perpendicular bisectors of a circle are described as:

the lines passing through the midpoint of each side of which are perpendicular to the given side.

Below is a triangle with one perpendicular bisector running through side AB .

The circumcircle of a triangle is:

The point of concurrency of the 3 perpendicular bisectors of each side of the triangle.

The centre point of the circumcircle is the point of intersection of all the perpendicular bisectors of a triangle.

Below is a triangle with all 3 perpendicular bisectors and the circumcircle drawn with d3.js.

The first step was to draw one perpendicular bisector of a triangle.

I chose 3 arbitary points for the vertices of the triangle.

points.js
1
2
3
4
5
const points = {
  a: {x: xScale(1), y: yScale(1)},
  b: {x: xScale(5), y: yScale(19)},
  c: {x: xScale(17), y: yScale(6)}
};

This is all the information I need, to calculate the perpendicular bisectors and the circumcircle.

If I wanted to find the perpendicular bisector of AB using pen and paper, I would perform the following steps:

  • I would find the gradient or slope (for US readers) of the point AB.
  • I would then find the perpendicular gradient or slope which would give me the ratio of rise over run that the perpendicular line flows through. If lines are perpendicular then M1 x M2 = -1.
  • I would find the midpoint of the line using the distance formula ((x1 + x2 / 2), (y1 + y2 / 2)).
  • I could then plug these values into the equation of a line which takes the form of y = mx + c.

I have blogged previously in this post about how to set up the graduated x and y axis and a more managable scale for positioning vertices etc.

My first step was to find the perpendicular bisector of the line AB.

Below are two helper functions that take javascript point objects as arguments with x and y properties that map to coordinates and return either a gradient/slope or the perpendicular gradient/slope that occurrs between the 2 coordinates:

gradient.js
1
2
3
4
5
6
7
const gradient = function(a, b) {
  return ((b.y - a.y) / (b.x - a.x));
};

const perpendicularGradient = function (a, b) {
  return -1 / gradient(a, b);
};

Below is a helper function to find the midpoint between two vertices or points:

midpoint.js
1
2
3
const midpoint = function(a, b) {
return {x: ((a.x + b.x) / 2), y: ((a.y + b.y) / 2)};
};

Using these values, I can then find the y-intercept or the point where the perpendicular line will cut the y-axis.

Below is a function that will find the y-intercept given a vertex and a gradient/slope:

yintercept.js
1
2
3
function getYIntercept(vertex, slope) {
  return vertex.y - (slope * vertex.x);
}

You can think of the above function as rearranging y = mx + c to solve for c or c = y - mx.

All that is left is to find the x-intercept or the point where the bisector line cuts the x-axis.

Below is the code that brings this all together:

perpendicular-bisector.js
1
2
3
4
5
function perpendicularBisector(a, b) {
  const slope = perpendicularGradient(a, b),
        midPoint = midpoint(a, b),
        yIntercept = getYIntercept(midPoint, slope),
        xIntercept =  - yIntercept / (slope);

The x-intercept on line 5 is again re-arranging the equation of the line formula y = mx + c to solve for x.

The finshed function looks like this and there are a number of if statements I had to add for the conditions when the slope or gradient function might end up undefined or equalling infinity when it encounters horizontal or vertical values that have catches with the formula. I would love to know if there is an algorithm that will avoid such checks:

perpendicularBisector.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function perpendicularBisector(a, b) {
  const slope = perpendicularGradient(a, b),
        midPoint = midpoint(a, b),
        yIntercept = getYIntercept(midPoint, slope),
        xIntercept =  - yIntercept / (slope);

  if((yIntercept === Infinity || yIntercept === -Infinity)) {
    return drawTriangleLine(g, {
      x1: xScale(midPoint.x),
      y1: yScale(0),
      x2: xScale(midPoint.x),
      y2: yScale(20)
    });
  }

  if((a.x === b.x) || isNaN(xIntercept)) {
    return drawTriangleLine(g, {
      x1: xScale(0),
      y1: yScale(midPoint.y),
      x2: xScale(20),
      y2: yScale(midPoint.y)
    });
  }

  if(xIntercept < 0 || yIntercept < 0) {
    return drawTriangleLine(g, {
      x1: xScale(xIntercept),
      y1: yScale(0),
      x2: xScale(20),
      y2: yScale((slope * 20) + yIntercept)
    });
  }

  drawTriangleLine(g, {
      x1: xScale(xIntercept),
      y1: yScale(0),
      x2: xScale(0),
      y2: yScale(yIntercept)
    });

  return {vertex: midPoint, slope: slope};
}

The drawTriangleLine function looks like this and simply adds a d3.js line:

drawTriangleLine.js
1
2
3
4
5
6
7
8
9
const drawTriangleLine = function drawTriangleLine(group, vertices) {
  group.append('line')
    .style('stroke', 'green')
    .attr('class', 'line')
    .attr('x1', vertices.x1)
    .attr('y1', vertices.y1)
    .attr('x2', vertices.x2)
    .attr('y2', vertices.y2);
};

Every time I call the perpendicularBisector function, I return an object that contains a vertex and point that I can use to draw the circumcircle.

return.js
1
return {vertex: midPoint, slope: slope};

All that is left is to draw the circumcircle and here is the function I wrote to do just that:

circumcircle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function drawCirumCircle(lineA, lineB) {
  if(!lineA || !lineB) {
    return;
  }

  const x1 = - lineA.slope,
      y1 = 1,
      c1 = getYIntercept(lineA.vertex, lineA.slope),
      x2 = - lineB.slope,
      y2 = 1,
      c2 = getYIntercept(lineB.vertex, lineB.slope);

  const matrix = [
    [x1, y1],
    [x2, y2]
  ];

  const circumCircleCentre = solveMatrix(matrix, [c1, c2]),
      dist = distance(convertPoint(points.b), circumCircleCentre);

  g.append('circle')
   .attr('cx', xScale(circumCircleCentre.x))
   .attr('cy', yScale(circumCircleCentre.y))
   .attr('r', xScale(dist))
   .attr('class', 'circumcircle')
   .attr('fill-opacity', 0.0)
   .style('stroke', 'black');
}

In order to find the centre of the circumcirle or the point of intersection of the perpendicular bisectors, the function takes two arguments lineA and lineB which are two of the perpendicular bisectors of the traingle. The function then arranges these line objects into y = mx + c format on lines 6 to 11 of the above. I then solve these equations simulataneously using matrices and specifically using cramer’s rule to find the point where the line intersect.

Once I have the 2x2 matrix assembled on lines 13-16, I then pass it to the solveMatrix function with the 2 y-intercept values that will apply cramer’s rule:

solveMatrix.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function det(matrix) {
  return (matrix[0][0]*matrix[1][1])-(matrix[0][1]*matrix[1][0]);
}

function solveMatrix(matrix, r) {
   const determinant = det(matrix);
   const x = det([
      [r[0], matrix[0][1]],
      [r[1], matrix[1][1]]
    ]) / determinant;

   const y = det([
     [matrix[0][0], r[0]],
     [matrix[1][0], r[1]]
   ]) / determinant;

  return {x: Math.approx(x), y: Math.approx(y)};
}

I now have the point of intersection of the perpendicular bisectors. All I need to know now is the radius of the circle. The calculation I used is to use the distance formula. From the point of intersection we just found to one of the vertices of the triangle.

Below is a helper function for the distance formuala:

distance.js
1
2
3
function distance(a, b) {
  return Math.floor(Math.sqrt(Math.pow((b.x - a.x), 2) + Math.pow((b.y - a.y), 2)));
}

All I have to do now is draw the circle from the two knowns, i.e. the point of intersection and the radius:

circle.js
1
2
3
4
5
6
7
8
9
10
  const circumCircleCentre = solveMatrix(matrix, [c1, c2]),
      dist = distance(convertPoint(points.b), circumCircleCentre);

  g.append('circle')
   .attr('cx', xScale(circumCircleCentre.x))
   .attr('cy', yScale(circumCircleCentre.y))
   .attr('r', xScale(dist))
   .attr('class', 'circumcircle')
   .attr('fill-opacity', 0.0)
   .style('stroke', 'black');

Here is a working jsbin that illustrates all I have wrote about.

I have also added drag and drop so you can drag the vertices around by the red circles and watch it all redraw.

Please leave a comment below if I could have achieved this in a more efficient way.

Comments