Tutorial

Tutorial

This tutorial is available in the Jupyter notebook format, together with other example notebooks, in the doc folder. To open the Jupyter notebook in the correct folder, simply type:

using IJulia, Interact
notebook(dir = Interact.notebookdir)

in your Julia REPL. You can also view it online here.

Installing everything

To install Interact, simply type

Pkg.add("Interact")

in the REPL.

The basic behavior is as follows: Interact provides a series of widgets. Each widget has an output that can be directly inspected or used to trigger some callbacks (i.e. run some code as soon as the widget changes value): the abstract supertype that gives this behavior is called AbstractObservable. Let's see this in practice.

Displaying a widget

using Interact
ui = button()
display(ui)

Note that display works in a Jupyter notebook or in Atom/Juno IDE. Interact can also be deployed in Jupyter Lab, but that requires installing an extension first:

cd(Pkg.dir("WebIO", "assets"))
;jupyter labextension install webio
;jupyter labextension enable webio/jupyterlab_entry

To deploy the app as a standalone Electron window, one would use Blink.jl:

using Blink
w = Window()
body!(w, ui);

The app can also be served in a webpage via Mux.jl:

using Mux
WebIO.webio_serve(page("/", req -> ui), rand(8000:9000)) # serve on a random port

Adding behavior

The value of our button can be inspected using getindex:

ui[]

In the case of a button, the observable represents the number of times it has been clicked: click on it and check the value again. For now however this button doesn't do anything. This can be changed by adding callbacks to it.

To add some behavior to the widget we can use the on construct. on takes two arguments, a function and an AbstractObservable. As soon as the observable is changed, the function is called with the latest value.

on(println, ui)

If you click again on the button you will see it printing the number of times it has been clicked so far.

Tip: anonymous function are very useful in this programming paradigm. For example, if you want the button to say "Hello!" when pressed, you should use:

on(n -> println("Hello!"), ui)

Tip n. 2: using the [] syntax you can also set the value of the widget:

ui[] = 33;

Observables: the implementation of a widget's output

The updatable container that only has the output of the widget but not the widget itself is a Observable and can be accessede using observe(ui), though it should normally not be necessary to do so. To learn more about Observables and AbstractObservable, check out their documentation here.

What widgets are there?

Once you have grasped this paradigm, you can play with any of the many widgets available:

filepicker() |> display # value is the path of selected file
textbox("Write here") |> display # value is the text typed in by the user
autocomplete(["Mary", "Jane", "Jack"]) |> display # as above, but you can autocomplete words
checkbox(label = "Check me!") |> display # value is a boolean describing whether it's ticked
toggle(label = "I have read and agreed") |> display # same as a checkbox but styled differently
slider(1:100, label = "To what extent?", value = 33) |> display # value is the number selected

As well as the option widgets, that allow to choose among options:

dropdown(["a", "b", "c"]) |> display # value is option selected
togglebuttons(["a", "b", "c"]) |> display # value is option selected
radiobuttons(["a", "b", "c"]) |> display # value is option selected

The option widgets can also take as input a dictionary (ordered dictionary is preferrable, to avoid items getting scrambled), in which case the label displays the key while the output stores the value:

s = dropdown(OrderedDict("a" => "Value 1", "b" => "Value 2"))
display(s)
s[]

Creating custom widgets

Interact allows the creation of custom composite widgets starting from simpler ones. Let's say for example that we want to create a widget that has three sliders and a color that is updated to match the RGB value we gave with the sliders.

import Colors
using Plots

function mycolorpicker()
    r = slider(0:255, label = "red")
    g = slider(0:255, label = "green")
    b = slider(0:255, label = "blue")
    output = Interact.@map Colors.RGB(&r/255, &g/255, &b/255)
    plt = Interact.@map plot(sin, color = &output)
    wdg = Widget(["r" => r, "g" => g, "b" => b], output = output)
    @layout! wdg hbox(plt, vbox(:r, :g, :b)) ## custom layout: by default things are stacked vertically
end

And now you can simply instantiate the widget with

mycolorpicker()

Note the &r syntax: it means automatically update the widget as soon as the slider changes value. See Interact.@map for more details. If instead we wanted to only update the plot when a button is pressed we would do:

function mycolorpicker()
    r = slider(0:255, label = "red")
    g = slider(0:255, label = "green")
    b = slider(0:255, label = "blue")
    update = button("Update plot")
    output = Interact.@map (&update; Colors.RGB(r[]/255, g[]/255, b[]/255))
    plt = Interact.@map plot(sin, color = &output)
    wdg = Widget(["r" => r, "g" => g, "b" => b, "update" => update], output = output)
    @layout! wdg hbox(plt, vbox(:r, :g, :b, :update)) ## custom layout: by default things are stacked vertically
end

A simpler approach for simpler cases

While the approach sketched above works for all sorts of situations, there is a specific macro to simplify it in some specific case. If you just want to update some result (maybe a plot) as a function of some parameters (discrete or continuous) simply write @manipulate before the for loop. Discrete parameters will be replaced by togglebuttons and continuous parameters by slider: the result will be updated as soon as you click on a button or move the slider:

width, height = 700, 300
colors = ["black", "gray", "silver", "maroon", "red", "olive", "yellow", "green", "lime", "teal", "aqua", "navy", "blue", "purple", "fuchsia"]
color(i) = colors[i%length(colors)+1]
ui = @manipulate for nsamples in 1:200,
        sample_step in slider(0.01:0.01:1.0, value=0.1, label="sample step"),
        phase in slider(0:0.1:2pi, value=0.0, label="phase"),
        radii in 0.1:0.1:60
    cxs_unscaled = [i*sample_step + phase for i in 1:nsamples]
    cys = sin.(cxs_unscaled) .* height/3 .+ height/2
    cxs = cxs_unscaled .* width/4pi
    dom"svg:svg[width=$width, height=$height]"(
        (dom"svg:circle[cx=$(cxs[i]), cy=$(cys[i]), r=$radii, fill=$(color(i))]"()
            for i in 1:nsamples)...
    )
end

or, if you want a plot with some variables taking discrete values:

using Plots

x = y = 0:0.1:30

freqs = OrderedDict(zip(["pi/4", "π/2", "3π/4", "π"], [π/4, π/2, 3π/4, π]))

mp = @manipulate for freq1 in freqs, freq2 in slider(0.01:0.1:4π; label="freq2")
    y = @. sin(freq1*x) * sin(freq2*x)
    plot(x, y)
end

Widget layout

To create a full blown web-app, you should learn the layout tools that the CSS framework you are using provides. See for example the columns and layout section of the Bulma docs. You can use WebIO to create from Julia the HTML required to create these layouts.

However, this can be overwhelming at first (especially for users with no prior experience in web design). A simpler solution is CSSUtil, a package that provides some tools to create simple layouts.

loadbutton = filepicker()
hellobutton = button("Hello!")
goodbyebutton = button("Good bye!")
ui = vbox( # put things one on top of the other
    loadbutton,
    hbox( # put things one next to the other
        pad(1em, hellobutton), # to allow some white space around the widget
        pad(1em, goodbyebutton),
    )
)
display(ui)

Update widgets as function of other widgets

Sometimes the full structure of the GUI is not known in advance. For example, let's imagine we want to load a DataFrame and create a button per column. Not to make it completely trivial, as soon as a button is pressed, we want to plot a histogram of the corresponding column.

Important note: this app needs to run in Blink, as the browser doesn't allow us to get access to the local path of a file.

We start by adding a filepicker to choose the file, and only once we have a file we want to update the GUI. this can be done as follows:

loadbutton = filepicker()
columnbuttons = Observable{Any}(dom"div"())

columnbuttons is the div object that will contain all the relevant buttons. it is an Observable as we want its value to change over time. To add behavior, we can use map!:

using CSV, DataFrames
data = Observable{Any}(DataFrame)
map!(CSV.read, data, loadbutton)

Now as soon as a file is uploaded, the Observable data gets updated with the correct value. Now, as soon as data is updated, we want to update our buttons.

function makebuttons(df)
    buttons = button.(names(df))
    dom"div"(hbox(buttons))
end

map!(makebuttons, columnbuttons, data)

We are almost done, we only need to add a callback to the buttons. The cleanest way is to do it during button initialization, meaning during our makebuttons step:

using Plots
plt = Observable{Any}(plot()) # the container for our plot
function makebuttons(df)
    buttons = button.(string.(names(df)))
    for (btn, name) in zip(buttons, names(df))
        map!(t -> histogram(df[name]), plt, btn)
    end
    dom"div"(hbox(buttons))
end

To put it all together:

using CSV, DataFrames, Interact, Plots
loadbutton = filepicker()
columnbuttons = Observable{Any}(dom"div"())
data = Observable{Any}(DataFrame)
plt = Observable{Any}(plot())
map!(CSV.read, data, loadbutton)

function makebuttons(df)
    buttons = button.(string.(names(df)))
    for (btn, name) in zip(buttons, names(df))
        map!(t -> histogram(df[name]), plt, btn)
    end
    dom"div"(hbox(buttons))
end

map!(makebuttons, columnbuttons, data)

ui = dom"div"(loadbutton, columnbuttons, plt)

And now to serve it in Blink:

using Blink
w = Window()
body!(w, ui)

This page was generated using Literate.jl.