Makie animations to see if it's viable.
So far, I haven't found a viable use case for this yet since the base
Plots library in Julia is so good and the syntax there is more intuitive.
# using GLMakie using CairoMakie # Use a custom theme for fun include("cyberpunk_theme.jl") set_theme!(cyberpunk_theme)
From the docs:
To create an animation you need to use the
- First you create a
- Next, you pass a function that modifies this figure frame-by-frame to record. Any changes you make to the figure or its plots will appear in the final animation.
- You also need to pass an iterable which has as many elements as you want frames in your animation. The function that you pass as the first argument is called with each element from this iterator over the course of the animation.
Let's try to keep this example really, really simple: animate a dot that's moving up and to the right as time passes by.
To do that, I think we have to understand two key concepts:
Observables and listeners.
Observable: A variable that changes with each frame in your animation
Observable is something that
Makie is designed to 'listen' to and react to changes. We declare an
Observable like below:
time = Observable(0.0)
The above code created an
time that currently has the value of 0.0. We can update the current value of
time by simply using this empty bracket notation:
time = 10 time
@lift macro to define a relationship with an
In this example, both the
y coordinates will depend only on
x = @lift(1 + $time) y = @lift(5 + 0.5*($time)) # look at time time
Observable(10.0) 0 => map((::var"#1#2")(arg1) in Main) 0 => map((::var"#3#4")(arg1) in Main)
If we look at
time again, we should see that it has 2 listeners now.
After we've created our
Observable and listeners
y, we need to follow 3 steps to create our animation:
time, which in turn, changes
# 0. Create `Observable` and listeners `x` and `y`: time = Observable(0.0) time = 10 # initial value x = @lift(1 + $time) y = @lift(5 + 0.5*($time)) # 1. Create a figure fig = scatter(x, y, color = :red, markersize = 20, axis = (title = @lift("t = $(round($time, digits =1))"),)) # 2. Next, pass a function that modifies this figure frame by frame to record. """ Update the value of observable `time` to be equal to t, where t is from the timestamps iterator (i.e. a value between 0 and 5) """ function move_dot(t) time = t end # 3. pass an iterable that has the same number of elements as frames in the animation framerate = 30 timestamps = range(0, 5, step=1/framerate) # 4. Create an animation from the function record(move_dot, fig, "moving_dot.gif", timestamps; framerate = framerate) # # Alternate with `do` notation to make an anonymous function: # record(fig, "moving_dot.gif", timestamps; # framerate = framerate) do t # time = t # end
What this code does:
tin the iterator
timestamps(which is a
rangebetween 0 and 5)
move_dotfunction with a single
What about using a
Point2f type as the
Observable? Instead of setting
y as listeners that depend on
time, we could instead change the type of
Observable so that it's a 2d point.
# 0. Create a 2-d `Observable`/listener combo location = Observable(Point2f(0,0)) # 1. Create a figure fig, ax, scatterplot = scatter(location, color = :red, markersize = 20, axis = (title = @lift("t = $($location)"),)) # Pro tip: The scale of the axis doesn't update automatically as the dot moves, # so manually set your limits! limits!(ax, 0, 30, 0, 30) # 2. Next, pass a function that modifies this figure frame by frame to record. """Update the point location at each frame""" function move_dot2(i) location = location + [0.5, 0.75] end # 3. Pass an iterable that has the same number of elements as frames in the animation framerate = 30 frames = 1:framerate # 4. Create an animation with `record` record(move_dot2, fig, "moving_dot2.gif", frames; framerate = framerate)
Now, let's step it up a little and have this dot move a little more randomly. I want this dot to do a random walk.
# 0. Create a 2-d `Observable`/listener combo location = Observable(Point2f(0,0)) # 1. Create a figure fig, ax, scatterplot = scatter(location, color = :red, markersize = 20, axis = (title = @lift("location = $($location))"),)) # Pro tip: The scale of the axis doesn't update automatically as the dot moves, # so manually set your limits! limits!(ax, -10, 10, -10, 10) # 2. Next, pass a function that modifies this figure frame by frame to record. """Take a random step in the x or y direction""" function random_walk(i) xStep = (rand() * 2)-1 yStep = (rand() * 2)-1 # Update the location with a step between -1 and 1 in x and y directions location = location + [xStep, yStep] end # 3. Pass an iterable that has the same number of elements as frames in the animation framerate = 10 frames = 1:30 # 4. Create an animation with `record` record(random_walk, fig, "random_walk.gif", frames; framerate = framerate)
Next, we'll compare the output from base
Plots to animate a line plot. Here's an example of the syntax for animating a line plot with
using Plots # Simple dataset of 50 random numbers y = rand(50) # Use the @animate macro in front of a loop to create each frame of your animation anim = @animate for i in 1:50 # At each iteration we will plot a new subset of the data # I set the xlimits and ylimits so the x/y axes don't autofit as the data grows plot(y[1:i], xlims=(1,50), ylims=(0,1)) end # Save the animation as a gif and display it gif(anim)
The above code should create a line plot where the lines grow from left to right.
Let's see how
using Random Random.seed!(42) y = rand(50) x = range(1,50) fig = lines(x,y)
Next let's animate it using the
Point2f syntax to hold our points. An
Observable is an array so the value will start at (0,0) but we'll append new points to the array for each frame in the animation.
points = Observable(Point2f[(0, 0)]) fig, ax, lineplot = lines(points) limits!(ax, 0, 50, 0, 1) function update_lines(i) new_point = Point2f(x[i], y[i]) # Observables are arrays, so you can add to them points = push!(points, new_point) end record(update_lines, fig, "moving_lines.gif", 1:50; framerate=12)
Here's how I'd use animations - Imagine we had a time series of historical datapoints and we want to visualize the uncertainty behind our forecast. Typically, you'd show the prediction interval as two horizontal lines that would indicate the high/low boundaries of the prediction interval. But another way to think about that interval is that there are some infinite number of point forecasts that we could generate, each one a possible version of the future. Then, the prediction interval should include some large percentage (say, 95%) of these possible futures.
We could show that as an animation, and use a mutating function like
lines! here to show how different forecasts might look:
fig, ax, lineplot = lines(x, y) limits!(ax, 0, 70, 0, 1) new_x = 51:70 # Imagine this is a matrix where each row is a separate forecast for 20 # periods into the future forecast_matrix = rand(Float64, (30,20)) # new_y = Observable(forecast_matrix[1,:]) new_y = Observable(forecast_matrix[1,:]) function step_through_forecasts(i) new_y = forecast_matrix[i,:] end lines!(new_x, new_y) record(step_through_forecasts, fig, "forecast_uncertainty.gif", 1:20; framerate=2)