Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso

Animating a Sine Wave With d3.js and MathJax

Below is an animated gif of the end result of this post or you can see the real page here.

The full and current source can be found here.

I have spent the last year learning some of the maths I should have learned about 27 years ago. One of the things that I have found interesting while learning maths is the relationship between the unit circle and a sine wave graph of y = sin(x).

A sine wave is a mathmatical curve that describes a smooth repetitive oscillation and the unit circle is a circle of radius 1 centred at the origin (0, 0). The unit circle can be used to find special trigonometric ratios as well as aid in graphing. There is also a real number line wrapped around the circle that serves as the input value when evaluating trig functions such as sine and cosine.

A sine wave is a periodic function or a function that repeats itself at regular intervals. The most important examples of periodic functions are the trigonometric functions that repeat themselves over intervals of . One journey around the unit circle is 360 degrees or in radians. A sine wave shows the excursion around the circle happening in time and is ultimately a circle expressed in time. I have used d3.js to illlustrate how the journey around the circle corresponds to the sine wave movement over time.

There are a number of concepts that I wanted to capture in the animation:

  1. Illustrate where the input value of the unit circle corresponds to the (x, y) coordinate on the horizontal axis of the sine wave.

  2. Demonstrate how the right angle is formed by the angle of the radius moving counter-clockwise around the unit circle.

  3. Show the number scale of the unit circle in radians as ratios in proper mathmatical notation in the browser in both the unit circle and the x axis of the sine graph. MathJax appears to be the only show in town that fits this requiremnt.

This process of creating graphs from the unit circle is often called unwrapping the unit circle.

Unit Circle Setup

The first step is to create the basic shapes that will illustrate the unit circle and use cartesian coordinates to position them. D3.js has an excelent scale abstraction that allows you to deal in a finer grained scale than pixels. I can now think of the dimensions of the svg document as a 20 x 20 grid which makes positioning things easier to reason about.

scale.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  componentDidMount() {
    const el = this.refs.sine;

    const dimensions = this.getDimensions();

    const xScale = d3.scale.linear()
            .domain([0, 20])
            .range([0, dimensions.width]);

    const yScale = d3.scale.linear()
            .domain([0, 20])
            .range([dimensions.height, 0]);

    const svg = d3.select(el).append("svg")
            .attr('class', 'svg-container')
            .attr("width", dimensions.width)
            .attr("height", dimensions.height);

On lines 6 and 10 of the above code, horizontal x and vertical y scales are created and bound to two variables. These d3 scale functions take the input domain of 0 to 20 and map it to an output range of either the viewport width for the horizontal x axis or the viewport height for the vertical y axis. It is much easier to think of cartesian coordinates ranging from 0 to 20 than the very fine grained pixel scale.

With the scales in place, it is much easier to start positioning the elements:

shapes.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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    const initialX = xScale(12);
    const initialY = yScale(15);

    const firstAxisXCoord = -(radius * 1.5);

    const graphContainer = container.append("g")
            .attr("class", "circle-container")
            .attr('transform', `translate(${initialX}, ${initialY})`);

    graphContainer.append('circle')
      .attr('cx', 0)
      .attr('cy', 0)
      .attr('r', radius)
      .attr('class', 'unit-circle')
      .style('fill', 'none');

    const hypotenuse = graphContainer.append('line')
            .attr('class', 'hypotenuse')
            .attr('x1', 0)
            .attr('y1', 0)
            .attr('x2', 0)
            .attr('y2', 0);

    const opposite = graphContainer.append('line')
            .attr('class', 'opposite')
            .attr('x1', 0)
            .attr('y1', 0)
            .attr('x2', 0)
            .attr('y2', 0);

    const adjacent = graphContainer.append('line')
            .attr('class', 'adjacent')
            .attr('x1', 0)
            .attr('y1', 0)
            .attr('x2', 0)
            .attr('y2', 0);

    const dot = graphContainer.append('circle')
            .attr('cx', radius)
            .attr('cy', 0)
            .attr('r', 5)
            .attr('class', 'circle-guide')
            .attr('fill-opacity', 0.1);

    const verticalDot = graphContainer.append('circle')
            .attr('cx', 0)
            .attr('cy', 0)
            .attr('r', 5)
            .attr('class', 'vertical-guide')
            .attr('fill-opacity', 0.1);

    const joiningLine = graphContainer.append('line')
            .attr('class', 'joining-line')
            .attr('x1', firstAxisXCoord)
            .attr('y1', 0)
            .attr('x2', 0)
            .attr('y2', 0);

An svg group element or a g element is created on line 5. All elements will be added to this group or container. Transformations applied to an svg group or g element are applied to all elements in the group which makes it extremely useful.

The three lines that will form the right angle triangle are created and bound to the adjacent, opposite and hypotenuse variables using the d3.js svg line shape. The initial values of the triangle line’s x1, x2, y1 and y2 coordinates are irrelevant at this stage, they will be set on each tick of the animation. It is just important to get them onto the document during the initialisation phase.

The larger unit circle is created and appended onto the group on line 10 of the above snippet and another smaller circle is added on line 38 which will be positioned counter-clockwise around the large circle by setting its cx and cy coordinates on each tick of the animation. This will give the impression that the small circle is rotating around the larger circle.

Below is how things look so far:

The next stage is to add the number line in radians of the unit circle:

Number Scale

In order to divide the circle into 8 lines and position the labels, I can kill two birds with one stone by creating an array of objects which have label and angle properties that I can iterate over and create the lines and labels:

scale.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
addRadianNumberLine(container) {
  [
    {val: Math.PI/4, label: "$\\frac" + "{\\pi}4$"},
    {val: Math.PI/2, label: "$\\frac" + "{\\pi}2$"},
    {val: (3 * Math.PI) / 4, label: "$\\frac" + "{3\\pi}4$"},
    {val: Math.PI, label: "$\\pi$"},
    {val: (5 * Math.PI) / 4, label: "$\\frac" + "{5\\pi}4$"},
    {val: (3 * Math.PI) / 2, label: "$\\frac" + "{3\\pi}2$"},
    {val: (7 * Math.PI) / 4, label: "$\\frac" + "{7\\pi}4$"},
    {val: (2 * Math.PI), label: "${2\\pi}$"},
  ].forEach((ray) => {
    const cosX = radius * Math.cos(ray.val);
    const sinY = radius * -Math.sin(ray.val);

    const offsetX = (ray.val > Math.PI / 2 && ray.val < (3 * Math.PI) / 2)  ? -20 : -5;
    const offsetY = (ray.val > 0 && ray.val < Math.PI)  ? -35 : 0;

    container.append('g')
      .attr('class', 'tick')
      .attr('transform', `translate(${cosX + offsetX}, ${sinY + offsetY})`)
      .append('text')
      .text(() => ray.label);

    container.append('line')
      .attr('x1', 0)
      .attr('y1', 0)
      .attr('x2', cosX)
      .attr('y2', sinY);
  });
}

Lines 2 to 11 define the array of objects to iterate over. Each object contains a val property that can be thought of as an angle to move counter-clockwise around the unit circle for each element in the array. There is also a label property which is the text of the angle in radians. The cryptic syntax of the label is written in LaTex, which allows you to define mathmatical notation that can be displayed in the browser. Mathjax will later parse these labels into authentic looking math symbols in the browser. More on my difficulties with MathJax at the end of the post.

On lines 12 and 13 the x and y coordinates are determined for each angle value in the array. By finding the cos(x) * radius and sin(x) * radius of each angle in the iteration of objects, I can ascertain the (x, y) coordinates of the point on the circumference of where to position the radian label and of where do draw the dividing line that will show the number scale on my unit circle.

Lines 15 to 16 add offset values for each x and y coordinate to ensure the labels all appear outside of the circle and not flush on the circumference.

Line 18 adds a new svg group element and the textual label is added to this newly created group.

Line 24 simply adds a line from the centre of the circle to the x and y coordinates on the circumference.

Before MathJax does the parsing, the circle looks like this:

A similar approach is used to create the sine graph axis but instead of iterating over an array, a pair of d3.js axis are created and latex syntax is used for the tickValues of the x-axis.

axis.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
addSineAxis(state) {
  const intTickFormat = d3.format('d');

  const yAxis = d3.svg.axis()
          .orient('left')
          .tickValues([-1, 0, 1])
          .tickFormat(intTickFormat)
          .scale(state.yScaleAxis);

  state.graphContainer
    .append('g')
    .attr('class', 'y axis left')
    .attr("transform", `translate(${state.firstAxisXCoord}, 0)`)
    .call(yAxis);

  const xTickValues = [0, 1.57, 3.14, 4.71, 6.28];

  const piMap = {'0': '0', '1.57': '\\pi\\over 2', '3.14': '\\pi', '4.71': '3\\pi\\over 2', '6.28': '2\\pi'};

  const xAxis = d3.svg.axis()
          .orient('bottom')
          .tickValues(xTickValues)
          .innerTickSize(0)
          .outerTickSize(0)
          .tickFormat((x) => `$${piMap[x]}$`)
          .scale(state.xScaleAxis);

  state.graphContainer
    .append('g')
    .attr('class', 'x axis left')
    .call(xAxis);
}

The basic setup is now complete:

Animating the unit circle

My first step is to create a hash of values that I have unimaginatively called state that serves as a container for all the elements and dimensions that I am going to need to transform the svg elements on the documents. I am going to pass this state object into the animation function that will set the new position of the elements on each tick of the animation. After that, the animation is kicked off with a call to drawGraph:

drawGraph.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const state = {
  initialX,
  initialY,
  firstAxisXCoord,
  graphContainer,
  xScaleAxis,
  yScaleAxis,
  dot,
  opposite,
  adjacent,
  hypotenuse,
  joiningLine,
  verticalDot,
  axisDot,
  time: 0,
  xIncrement: 0
};

this.drawGraph(state);

Arguably the most important value in the above is the time property on line 15 which will provide the x value of the horizontal value of the sine graph. This will be incremented on each tick of the animation.

The drawGraph function is below:

drawGraph.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
43
44
45
46
47
48
49
drawGraph(state) {
  const increase = ((Math.PI * 2) / 360);

  state.time += increase;
  state.xIncrement += increase;

  this.drawSineWave(state);

  if(state.xIncrement > (Math.PI * 2)) {
    state.xIncrement = increase;
  }

  const axisDotX = state.xScaleAxis(state.xIncrement);

  state.axisDot
    .attr('cx', axisDotX)
    .attr('cy', 0);

  const dx = radius * Math.cos(state.time);
  const dy = radius * -Math.sin(state.time); // counter-clockwise

  state.dot
    .attr('cx', dx)
    .attr('cy', dy);

  state.hypotenuse
    .attr('x2', dx)
    .attr('y2', dy);

  state.opposite
    .attr('x1', dx)
    .attr('y1', dy)
    .attr('x2', dx)
    .attr('y2', 0);

  state.adjacent
    .attr('x1', dx)
    .attr('y1', 0);

  state.verticalDot
    .attr('cy', dy);

  state.joiningLine
    .attr('y1', state.dot.attr('cy'))
    .attr('x2', state.dot.attr('cx'))
    .attr('y2', state.dot.attr('cy'));

  requestAnimationFrame(this.drawGraph.bind(this, state));
}

Every time the drawGraph function is called, the time property of the state variable is incremented by roughly 1 degree on line 4 to simulate time moving along the x axis. I do the same on line 5 for the circle that travels along the x axis. I have a separate counter as I need to reset it to zero each time the xIncrement variable exceeds .

Line 7 calls the drawSine function that draws the sine wave on each tick of the animation. More on this later.

Lines 9 to 17 positions the small circle that traverses along the horizontal x. The counter for this coordinate is reset each time it exceeds .

The rest of the code in this function takes care of finding and positoning the shapes on the unit circle to simulate the small circle rotating counter-clockwise on the unit circle and constructing the right angle triangle.

Lines 19 and 20 find the next x and y coordinates of the next point on the circumference to position the small circle that rotates the larger circle and bind them to the variables dy and dx. I use minus in the expression radius * -Math.sin(state.time) because we want to simultate rotating back counter-clockwise. Once we have this coordintate it is easy to position the three lines that make up the right angled triangle on lines 26 to 38.

Line 49 calls requestAnimationFrame which tells the browser that you wish to perform an animation and requests the browser call a specific function, drawGraph in this instance, before the next browser repaint.

I am also using a lesser known overload of bind on line 37 that creates a new partially applied function with some or all of the arguments already bound each time it is called. I have blogged about this previously here. In this case, the state hash will be bound every time the function is called because I am passing it into bind as an extra argument after this. I use this technique a lot in javascript land.

Animating the Sine wave

Now to the meat and potato of the piece, namely animating a smooth sine wave. The sine wave is mathmatically a very simple curve and a very simple graph. It is a simple x-y plot with the x-axis representing time and the y-axis representing angular displacement around the unit circle.

Below is the drawSineWave function that is called on each tick of the animation:

drawSine.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
drawSineWave(state) {
  d3.select('.sine-curve').remove();

  const sineData = d3.range(0, 54)
          .map(x => x * 10 / 84)
          .map((x) => {
            return {x: x, y: - Math.sin(x - state.time)};
          });

  const sine = d3.svg.line()
          .interpolate('monotone')
          .x( (d) => {return state.xScaleAxis(d.x);})
          .y( (d) => {return state.yScaleAxis(d.y);});

  state.graphContainer.append('path')
          .datum(sineData)
          .attr('class', 'sine-curve')
          .attr('d', sine);
  }

The fist step on line 2 is to remove the previously plotted curve before recreating it on each tick of the animation.

Lines 4 to 8 creates the points that will be used to plot the sine graph against.

One Line 4 the an array of integers is initialized using the d3 range function, this will create an array of 54 elements ranging from 0 to 53:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13...etc...53]

The upper bound of the array which is 53 is chosen to fill the available width in the viewport

I will come back to why the calculation on line 5 is needed but on line 6 I do a further map of the array that was initialised on line 4 to create an x and y coordinate for each element in the array that can be used to plot the sine wave. The x coordinate is simply the element of the array and y is calculated by getting the negative sine of x because we are simulating the sine wave moving counter-clockwise. For each tick of the animation we simply subtract the state.time variable that we have used previously from x. If you look at the animation, you can see how the small circle in the middle of the unit circle moves up and down at exactly the same rate. Maths in action!

Below is how the sine wave looks without the transformation .map(x => x * 10 / 84) applied to the original array:

If we just use integers to plot the points we get the rough sine wave but if we use floats, we get a much smoother flowing sine wave. I multiply each value by 10 to space it out across the width and then divide by 84 to ensure I get a float back. 84 was arrived at by trial and error to ensure the wave spans across the graph.

Once I have my coordinates to plot the curve, the following code takes care of creating the curve on each animation tick:

monotone.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  const sineData = d3.range(0, 54)
          .map(x => x * 10 / 85)
          .map((x) => {
            return {x: x, y: - Math.sin(x - state.time)};
          });

  const sine = d3.svg.line()
          .interpolate('monotone')
          .x( (d) => {return state.xScaleAxis(d.x);})
          .y( (d) => {return state.yScaleAxis(d.y);});

  state.graphContainer.append('path')
    .datum(sineData)
    .attr('class', 'sine-curve')
    .attr('d', sine);

Lines 7 to 10 define a d3 line function that will be used by the svg path element on lines 12 to 15. The d3 line function can be thought of as a path generator for a line or in this case a curve as the interpolation mode is set to montone interpolation in order to create a smooth curve for the sine wave. The line function will take the data array (sineData) and convert it into the rather cryptic svg path mini language instructions that the svg path element on lines 12 to 15 will use to construct the curve. We define accessor functions on lines 9 and 10 that will be called for every x and y coordinate of the sineData array on line 1. A monotone interpolation will then be performed by the path function for each point. Every x and y is mapped to the correct scale on lines 3 and 4.

Lines 12 to 15 attach the path element and sets its data via the datum attribute. The d attribute sets the path data or the mini language of path commands that the line function on line 7 will generate. The x and y accessors of the line function are invoked exactly once for each element in the array.

MathJax

All that remains is to tell MathJax to parse the latex into math symbols. I cannot believe how hard I found this. Below is how the code ended up after a lot of coffee, profanity and self doubt:

mathjax.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
addMathJax(svg) {
  const continuation = () => {
    MathJax.Hub.Config({
      tex2jax: {
        inlineMath: [ ['$','$'], ["\\(","\\)"] ],
        processEscapes: true
      }
    });

    MathJax.Hub.Register.StartupHook("End", function() {
      setTimeout(() => {
        svg.selectAll('.tick').each(function(){
          var self = d3.select(this),
               g = self.select('text>span>svg');

          if(g[0][0] && g[0][0].tagName === 'svg') {
            g.remove();
            self.append(function(){
              return g.node();
            });
          }
        });
      }, 500);
    });

    MathJax.Hub.Queue(["Typeset", MathJax.Hub, svg.node()]);
  };

  wait((window.hasOwnProperty('MathJax')), continuation.bind(this));
}

First of all MathJax did not seem to play nicely with react, I have to use a wait function to suspend execution until the MathJax is available. On lines 12 to 18 can best be desribed as extreme hackery. I have tagged any element on the svg document that contains latex with the tick css class. I add a hook to mathjax that is called whenever it has done its first parse, I then remove the svg element that was added by MathJax and re-add it again. This causes MathJax to reparse the markup and the symbols are rendered correctly. I don’t know if it was the fact that this is a react site that made this so difficult but it really did not play well with this site.

The wait function is below for completeness:

wait.js
1
2
3
4
5
6
7
export function wait(condition, func, counter = 0) {
  if(condition || counter > 10) {
    return func();
  }

  setTimeout(wait.bind(null, condition, func, (counter + 1)), 30);
};

Epilogue

I found this hugely enjoyable and I am more than satisfied with the result. This probably means I should get out more.

If you disagree with any of the above or can suggest better ways then please leave a comment below.

Comments