DEV Community

loading...
Cover image for Unwrapping Polar Coordinate Graph Animation with Svelte, D3.js, and SVG

Unwrapping Polar Coordinate Graph Animation with Svelte, D3.js, and SVG

harryli0088 profile image Harry Li Updated on ・4 min read

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

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

$: 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},
  ]
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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,
  ]
)
Enter fullscreen mode Exit fullscreen mode

y2

y2 simple moves from the center of the circle to the bottom of the svg, for every line.

$: y2 = $animation * halfHeight
Enter fullscreen mode Exit fullscreen mode

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]
)
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode
  $: lineData = ticks.map(t => getLineDataFromAngle(t.angle))
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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:




Discussion (0)

pic
Editor guide