Unwrapping Polar Coordinate Graph Animation with Svelte, D3.js, and SVG
Tags: svelte, d3js, animation, visualization
Published: 2020-12-31 18:18:53

I recently created my first Svelte app to interactively explain how Bearing Rate Graphs work: https://harryli0088.github.io/bearing-rate-graph/

As part of the explanation, I wanted to make an unwrapping polar coordinate graph animation like Smarter Every Day had done in his video: https://youtu.be/AqqaYs7LjlM?t=447

{% codepen https://codepen.io/harryli0088/pen/QWKrbvR default-tab=result %}

I created this animation using
- Svelte's motion `tweened` function, based off their tutorial: https://svelte.dev/tutorial/tweened

- Interpolations using D3.js' `scaleLinear`: https://github.com/d3/d3-scale

- SVG `line` and `path`

## How I implemented it

After looking closely at Smarter Every Day's animation, I realized that several steps are necessary to achieve the unwrapping effect:

- Interpolate the line endpoints from the center of the polar circle to the bottom of the BRG rectangle

- Interpolate the angle of the lines from polar to 
straight up and down

- Use some trigonometry to calculate the positions of the edges of the polar circle to the top of the BRG rectangle


## Step 1
We can start by hardcoding the polar coordinate graph with a circle and some ticks at major angles
```js
$: ticks = [ //hard code the tick positions
    {angle:180,     label: "180°",      x1: 0,                       y1: halfHeight,               x2: 0, y2: 0},
    {angle:225,     label: "",          x1: -halfWidth/Math.sqrt(2), y1: -halfHeight/Math.sqrt(2), x2: 0, y2: 0},
    {angle:270,     label: "270°",      x1: -halfWidth,              y1: 0,                        x2: 0, y2: 0},
    {angle:315,     label: "",          x1: -halfWidth/Math.sqrt(2), y1: halfHeight/Math.sqrt(2),  x2: 0, y2: 0},
    {angle:0,       label: "0° (360°)", x1: 0,                       y1: -halfHeight,              x2: 0, y2: 0},
    {angle:45,      label: "",          x1: halfWidth/Math.sqrt(2),  y1: -halfHeight/Math.sqrt(2), x2: 0, y2: 0},
    {angle:90,      label: "90°",       x1: halfWidth,               y1: 0,                        x2: 0, y2: 0},
    {angle:135,     label: "",          x1: halfWidth/Math.sqrt(2),  y1: halfHeight/Math.sqrt(2),  x2: 0, y2: 0},
    {angle:179.999, label: "",          x1: 0,                       y1: halfHeight,               x2: 0, y2: 0},
  ]
```
{% codepen https://codepen.io/harryli0088/pen/MWjGYZR default-tab=result %}

Full Step 1 Code: https://github.com/harryli0088/svelte-polar-animation-tutorial/blob/main/src/Step1.svelte

## Step 2
Next, we can introduce `tweened` from Svelte's motion package, which automatically updates values in the DOM. We can also set an interval to periodically change the value of `animation`.

```js
import { onDestroy } from 'svelte'
import { tweened } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'

const animation = tweened(0, {
  duration: 4000,
  easing: cubicOut
})
const interval = setInterval(() => {
  animation.set($animation===1 ? 0 : 1)
}, 5000)
onDestroy(() => clearInterval(interval))
```
{% codepen https://codepen.io/harryli0088/pen/yLajydW default-tab=result %}

Full Step 2 Code: https://github.com/harryli0088/svelte-polar-animation-tutorial/blob/main/src/Step2.svelte

## Step 3
Next we need to interpolate the `line` endpoints from the center of the polar circle to the bottom of the BRG rectangle.

### x2
The value of x2 changes of course depending on which angle we're looking at. In the animation, we can see that all angles start at the center of the circle. By the end of the animation, the angles (180° -> 360° or 0° -> 180°) end at x positions (left side -> center -> right side). We can represent this interpolation using D3.js' `scaleLinear` like this
```js
$: x2Scale = scaleLinear().domain(
  [0, 180, 180, 360]
).range(
  [
    0,
    $animation*halfWidth,
    -$animation*halfWidth,
    0,
  ]
)
```
### y2
y2 simple moves from the center of the circle to the bottom of the svg, for every line.

```js
$: y2 = $animation * halfHeight
```
{% codepen https://codepen.io/harryli0088/pen/MWjGwJW default-tab=result %}

Full Step 3 Code: https://github.com/harryli0088/svelte-polar-animation-tutorial/blob/main/src/Step3.svelte

## Step 4
Next comes the trickiest part. We want our lines to unwrap and position themselves straight up and down. This means that the angles of all the lines transition from the starting angle to 0°. For example, the line with angle 270° starts at 270° and ends up as 0°; the line at 135° starts at 135° and finishes at 0°. In my code, I call this transitioning angle `theta`. We just have to make sure that angles 180° and above transition towards 360°, and angles 180° and below transition towards 0°. (I accomplish the first part by subtracting those >=180° angles by 360°, so that 270° for example becomes -90°). 

```js
$: thetaScale = scaleLinear().domain(
  [0, 180, 180, 360]
).range(
  [0, (1 - $animation)*180, ($animation - 1)*180, 0]
)
```

Then we can take `theta` and use trigonometry to calculate the `line` endpoints `x1` and `y1`, 
```js
$: getLineDataFromAngle = (angle, radius=halfWidth) => {
  const x2 = x2Scale(angle)
  const theta = thetaScale(angle) / DEG_PER_RAD
  return {
    x1: x2 + radius * Math.sin(theta),
    y1: - radius * Math.cos(theta),
    x2, y2,
  }
}
```

```js
  $: lineData = ticks.map(t => getLineDataFromAngle(t.angle))
```

Also, instead of having a `circle`, which would not be feasible to animate in the way we need, we can fake a circle to line transition with SVG `path`, like this:

```js
const circleDegrees:number[] = []
for(let i=180; i<360; ++i) {
  circleDegrees.push(i)
}
for(let i=0; i<180; ++i) {
  circleDegrees.push(i)
}
circleDegrees.push(179.9) //this is now equal to [180, 181, ..., 359, 0, 1, ..., 179, 179.9]
$: topFactor = (12 + 3*$animation) / 16
$: circlePathRadius = halfWidth * topFactor
$: circlePath = circleDegrees.reduce(
  (d, degree) => {
    const { x1, y1 } = getLineDataFromAngle(degree, circlePathRadius)
    d += ` ${x1},${y1}`
    return d
  },
  "M"
)
```

{% codepen https://codepen.io/harryli0088/pen/VwKxLpr default-tab=result %}

Full Step 4 Code: https://github.com/harryli0088/svelte-polar-animation-tutorial/blob/main/src/Step4.svelte

## Final Result
Lastly we can add tweaks to the animation such as:
- Add padding for the time axis

- Transition in the time axis

- Dy changes for the angle labels

{% codepen https://codepen.io/harryli0088/pen/QWKrbvR default-tab=result %}


## Source Code
Repo: {% github harryli0088/svelte-polar-animation-tutorial no-readme %}
Final Svelte Component Code: {% gist https://gist.github.com/harryli0088/1e9f52c4fe0e573da4a8315b336cc44f %}