Interaction

Makie offers a sophisticated referencing system to share attributes across the Scene in your plot. This is great for interaction, animations and saving resources – also if the backend decides to put data on the GPU you might even share those in GPU memory.

Interaction and animations in Makie are handled by using Observables. An "observable", called Node in Makie, is a structure that can have its value updated interactively. Interaction, animations and more are done using Nodes and event triggers.

In this page we overview how the Nodes pipeline works, how event-triggering works, and we give an introduction to the existing "atomic" functions for interaction. Examples that use interaction can be found in the Examples/interaction page (see Example Gallery as well).

Have a peek at Animation for some more information once you're done with this.

Node interaction pipeline

The Node structure

A Node is a Julia structure that allows its value to be updated interactively. This means that anything that uses a Node could have its behavior updated interactively, as we will showcase in this page.

Let's start by creating a Node:

using Makie, AbstractPlotting
x = Node(0.0) # set up a Node, and give it a default value of 0.0
Observable{Float64} with 0 listeners. Value:
0.0

The value of the x can be changed by setting the empty index, i.e.:

x[] = 3.34;
3.34

Notice that you can access the value of a Node by indexing it with nothing, i.e. x[]. However, we recommend to use the function to_value to get the value of a Node, because to_value is a general function that works with all types instead of only Nodes. E.g.:

to_value(x)
3.34

Nodes depending on other Nodes

You can create a node depending on another node using lift:

f(a) = a^2
y = lift(a -> f(a), x)
Observable{Float64} with 0 listeners. Value:
11.1556

Now, for every value of the Nodex, the derived Nodey will hold the value f(x). Updating the value of xwill also update the value of y!

For example:

x[] = 10.0
for i in (x, y)
    println(to_value(i))
end
10.0
100.0

That is to say, the Nodey maps the function f (which is a -> a^2 in this case) on x whenever the Nodex is updated, and updates the corresponding value in y. This is the basis of updating Nodes, and is used for updating plots in Makie. Any plot created based on this pipeline system will get updated whenever the nodes it is based on are updated!

Note

For now, lift is just an alias for Observables.map, and Node is just an alias for Observables.Observable. This allows decoupling of the APIs.

Shorthand macro for lift

When using lift, it can be tedious to reference each participating Node at least three times, once as an argument to lift, once as an argument to the closure that is the first argument, and at least once inside the closure:

x = Node(rand(100))
y = Node(rand(100))
z = lift((x, y) -> x .+ y, x, y)
Observable{Array{Float64,1}} with 0 listeners. Value:
[0.6868569224113372, 0.839791404489941, 0.7874568309815109, 1.5648664960898677, 1.2908016807046705, 1.57259438716301, 0.7129937753291391, 1.6790193380213, 1.8387248820643327, 0.2755818504292391  …  0.9820588152490082, 0.7206167331838393, 1.1996015112639022, 1.3353969838580828, 1.2056012108899796, 0.4438595867142483, 0.766472367825733, 0.761720982373471, 0.1857690972204935, 1.0345829962293558]

To circumvent this, you can use the @lift macro. You simply write the operation you want to do with the lifted Nodes and prepend each Node variable with a dollar sign $. The macro will lift every Node variable it finds and wrap the whole expression in a closure. The equivalent to the above statement using @lift is:

z = @lift($x .+ $y)
Observable{Array{Float64,1}} with 0 listeners. Value:
[0.6868569224113372, 0.839791404489941, 0.7874568309815109, 1.5648664960898677, 1.2908016807046705, 1.57259438716301, 0.7129937753291391, 1.6790193380213, 1.8387248820643327, 0.2755818504292391  …  0.9820588152490082, 0.7206167331838393, 1.1996015112639022, 1.3353969838580828, 1.2056012108899796, 0.4438595867142483, 0.766472367825733, 0.761720982373471, 0.1857690972204935, 1.0345829962293558]

This also works with multiline statements and tuple or array indexing:

multiline_node = @lift begin
    a = $x[1:50] .* $y[51:100]
    b = sum($z)
    a .- b
end
Observable{Array{Float64,1}} with 0 listeners. Value:
[-105.11649947628382, -105.03464174605386, -104.69961497568647, -104.37355732715572, -104.63077711816366, -105.20003804515525, -104.71476691754188, -104.47001233284648, -104.66606055813465, -105.1327811608204  …  -105.20162995609522, -104.94525033081605, -104.88601507339789, -104.94232035495575, -104.78088670320625, -105.19051003932222, -105.04377061952754, -105.00074173145649, -105.18229605303354, -105.14556944566557]

Event triggering

Often it is the case that you want an event to be triggered each time a Node has its value updated. This is done using the on-do block from Observables. For example, the following code block "triggers" whenever x's value is changed:

on(x) do val
    println("x just got the value $val")
end
#9 (generic function with 1 method)

As you can see, at we have run this block in Julia, but nothing happened yet. Instead, a function was defined. However, upon doing:

x[] = rand(100);
100-element Array{Float64,1}:
 0.7355646853099362
 0.43363366908828427
 0.007908282447193216
 0.0343315752497273
 0.25370299091559856
 0.8228464461422091
 0.6785097908743918
 0.33032560100537944
 0.12030386448653552
 0.6213050166361955
 ⋮
 0.5529324112000906
 0.4784903538292138
 0.42366667136581637
 0.719501324645647
 0.5702222397046044
 0.12893134047523014
 0.5131911977222767
 0.1683764158818255
 0.5246590921231655

Boom! The event of the on-do block was triggered! We will be using this in the following paragraphs to establish interactivity.

For more info please have a look at Observables.

Atomic interaction functions

This section overviews some simple and specific functions that make interaction much simpler.

coming soon...

There are three principal plot elements that you can use to make your plot interactive. These are Slider, textslider, and Button.

Slider

Sliders are quite simple to make. They can be created by a call to the function slider, which usually takes the form:

sl = slider(range::AbstractVector, raw = true, camera = campixel!, start = somevalue)

which makes sl a Scene with only one slider. The slider will go through range, and start at somevalue. range must be a subtype of AbstractVector, meaning an Array{T, 1}, a LinRange, et cetera.

To access the value of the slider as an Observable, we can simply access sl[end][:value], which will return an Observable which will contain the value that the slider is on. You can then use that Observable in a call to lift.

A common way to use sliders is to hbox or vbox them with the Scene which depends on them.

Button

Buttons are clickable markers that can call a function on click, passing to it the number of clicks so far, on each click. A simple exmaple is as follows:

b1 = button("Test Button")
lift(b1[end].clicks) do clicks
    println("Button was clicked!")
    #Your function goes here
end

Textslider

Textsliders are a special case of sliders, with two key diferences - they automatically hbox a label with the slider, and they return a 2-tuple consisting of the Scene of the slider, and its value as an Observable. Usually, they will be called like so:

sl, ol = textslider(-1:0.01:1, "label", start = 0)

Interaction using the mouse

A few default Nodes are already implemented in a scene's Events (see them in scene.events), so to use them in your interaction pipeline, you can simply lift them.

For example, for interaction with the mouse cursor, lift the mouseposition signal.

pos = lift(scene.events.mouseposition) do mpos
    # do stuff
end

Interaction using the keyboard

To listen to keyboard events, you can liftscene.events.keyboardbuttons, which returns an enum that can be used with some utility functions to implement a keyboard event handler.

dir = lift(scene.events.keyboardbuttons) do but
    global last_dir
    ispressed(but, Keyboard.left) && return 1
    ispressed(but, Keyboard.up) && return 2
    ispressed(but, Keyboard.right) && time[] += 1
    ispressed(but, Keyboard.down) && return 0
    last_dir
end

<!–TODO make an actual example TODO can we make a keyboard viewer in Makie?–>

Recording your interactions

If you want to record the Scene you're interacting with, you can do that from within Makie. As an example, see the following code:

record(scene, "test.mp4"; framerate = 10) do io
    for i = 1:100        # sampling time
        sleep(0.1)       # sampling rate
        recordframe!(io) # record a new frame
    end
end

This will sample from the Scene scene for 10 seconds, at a rate of 10 frames per second.