Unwrapping Polar Coordinate Graph Animation with Svelte, D3.js, and SVG
Harry Li
Posted on December 31, 2020
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
I created this animation using
Svelte's motion
tweened
function, based off their tutorial: https://svelte.dev/tutorial/tweenedInterpolations using D3.js'
scaleLinear
: https://github.com/d3/d3-scaleSVG
line
andpath
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 downUse 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
$: 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},
]
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
.
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))
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
$: 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.
$: y2 = $animation * halfHeight
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°).
$: 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
,
$: 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,
}
}
$: 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:
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"
)
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
Source Code
Repo:
Final Svelte Component Code:
Posted on December 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.