Some Interact updates: create your custom widgets easily and composably

First of all, I’m happy to say that the Interact package (now based on InteractBase) was updated to Julia 1.0 (thanks to the amazing help of many people, esp. the heroic JuliaRobotics developers who fixed a large number of relevant packages).

Custom widgets

After a lengthy design discussion with @shashi at JuliaCon, we have finalized the recipe framework (a way for the user to define their own recipes), with a focus on simplicity and composability.

In short it works as follows: a Widget is basically a mutable struct with some components, an output and a layout. The Interact.@map macro allows to use Widgets like normal values (for example, a slider will be replaced by the numerical value it represents) and the result will be updated when it changes. Here for example we can create a widget with 3 components (color1, color2, legendpos) and a plot as output. The layout will be the default (components and then the output are disposed vertically one after another).

using Interact
using Interact: @map
using Colors, Plots

function myplot(color1, color2, legendpos)
    output = Interact.@map plot([sin, cos], color = [&color1 &color2], legend = &legendpos, label = ["sin" "cos"])
    Widget([:color1 => color1, :color2 => color2, :legendpos => legendpos], output = output)

ui = myplot(colorpicker(), colorpicker(), togglebuttons([:topleft, :topright, :bottomleft, :bottomright]));


However this design is very flexible, we can create our own colorpicker and use it instead:

function mycolorpicker()
    r = slider(0:255, label = "red")
    g = slider(0:255, label = "green")
    b = slider(0:255, label = "blue")
    output = @map Colors.RGB(&r/255, &g/255, &b/255)
    wdg = Widget(["r" => r, "g" => g, "b" => b], output = output) # it outputs a color, so it can be used as colorpicker
    @layout! wdg vbox(:r, :g, :b) # custom layout: only display the three sliders aligned vertically but don't display the output color

ui = myplot(mycolorpicker(), mycolorpicker(), togglebuttons([:topleft, :topright, :bottomleft, :bottomright]));


For more details be sure to checkout the Interact docs (esp. the Custom widgets section and the tutorial).


Interact apps are reasonably easy to deploy: see here for details. As an example, you can check out the GitHub repository a pure Julia website of Interact demos here. The site is not yet up online but you can easily set it up on your machine (or on your server) following the instructions on the README, then check it out at http://localhost:8000/ or yourserverIP:8000


To package developers: if you are interested in creating custom Widgets for your types and functions in your packages, now is a good time to get started! For example, StatPlots already has a GUI but this kind of simple UI to work with data makes sense in a lot of other packages (data manipulation packages, GLM) etc. Look at the custom widgets section of the docs if you have doubts, or feel free to ask here or in the Gizmos slack channel.


Some feedback starting from a freshly installed Julia 1.0, in case this helps:

I tried saving that code and running it (julia interact-demo.jl), and got the following error:

ERROR: LoadError: ArgumentError: Package Interact not found in current path:
- Run Pkg.add("Interact") to install the Interact package.

This is obvious – of course the Interact package must be installed to run a demo of the Interact package!

(As a small aside, the error message is incomplete (it should be import Pkg; Pkg.add("Interact"), but this has already been fixed in Julia master, via PR #28555)

Attempting to run the script after installing the Interact package produced a similar error, this time for the Colors package:

ERROR: LoadError: ArgumentError: Package Colors not found in current path:
- Run Pkg.add("Colors") to install the Colors package.

However, after installing Colors, the error was now a bit more cryptic:

ERROR: LoadError: UndefVarError: plot not defined
in expression starting at /home/waldyrious/interact-demo.jl:10

I assumed this means I should install Plots, so I did. After this, running the script still produced the same error as before, so I added a line with using Plots under the other “using” lines.

This time I could run the script without errors, but the program simply exits after a while, without doing anything. Am I missing something?

Ups, yes, you’ll need using Plots and to add all the required packages:

] add Plots Colors Interact

There are several way to visualizing the UI (corresponding in the example above to ui):

  • in the Jupyter notebook: it will appear inline automatically
  • in Juno: it should also be more or less automatic but Juno Interact integration in Julia 1.0 is WIP
  • in the REPL: you will need Blink to display it in a Blink window:
] add Blink
w = Window()
body!(w, ui)
  • in the browser (from your machine or a server):
] add Mux
WebIO.webio_serve(page("/", req -> ui))

and then open the browser at localhost:8000

This is all in the deploying section of the docs: that section could probably be expanded a bit.

EDIT: fixed webio_serve command

Ah, didn’t realize this was meant to be run in the REPL. I re-ran the first block of code in the REPL, followed by ]add Mux and using Mux, and then I was able to run WebIO.webio_serve(req -> ui):

julia> WebIO.webio_serve(req -> ui)
[ Info: Listening on: Sockets.InetAddr{Sockets.IPv4}(ip"", 0x1f40)
[ Info: Accept:  🔗    0↑     0↓    0s ≣16
[ Info: Closed:  💀    0↑     1↓🔒 130s ≣16
^C┌ Warning: Interrupted: listen(Sockets.InetAddr{Sockets.IPv4}(ip"", 0x1f40))
└ @ HTTP.Servers ~/.julia/packages/HTTP/nUK4f/src/Servers.jl:379

For some reason, however, nothing was served when visiting http://localhost:8000/ – the page simply remained in the loading stage until I closed it (as the command output above shows). Any hints what I might still be missing, @piever?

I had better luck with Blink:

julia> ] add Blink
julia> using Blink
julia> w = Window()
ERROR: Cannot find Electron. Try `Blink.AtomShell.install()`
julia> Blink.AtomShell.install()
w = Window()
Window(1, Electron(Process(`/home/waldyrious/.julia/packages/Blink/KMJt9/deps/atom/electron /home/waldyrious/.julia/packages/Blink/KMJt9/src/AtomShell/main.js port 4949`, ProcessRunning), Sockets.TCPSocket(RawFD(0x00000011) active, 0 bytes waiting), Dict{String,Any}("callback"=>##1#2())), Page(1, #undef, Dict{String,Any}("callback"=>##1#2()), Distributed.Future(1, 1, 1, Some(true))))

julia> body!(w, ui)
Page(1, WebSockets.WebSocket{Sockets.TCPSocket}(Sockets.TCPSocket(RawFD(0x00000018) active, 0 bytes waiting), true, CONNECTED::ReadyState = 1), Dict{String,Any}("webio"=>##77#78{BlinkConnection}(BlinkConnection(Page(#= circular reference @-4 =#))),"callback"=>##1#2()), Distributed.Future(1, 1, 1, Some(true)))

That finally allowed me to see the UI, although I kept getting these Gtk-Message: Failed to load module "pantheon-filechooser-module" warnings in the console. Anyway, just reporting my experience in case it helps others (or provides the perspective of a beginner to the developers of Interact :))

Thanks for the feedback!

That’s a bit worrying, I wonder whether it’s simply due to the Plots precompile so it seemed like nothing was happening: could you maybe try making some warm up plot and then running the app to see if something shows up? Otherwise do open an issue either at Mux or WebIO.

I tried a couple calls to plot(): plot([sin, cos]), plot([sin, cos, exp]), plot([sin, cos, log]) – all of which produced Gtk (IIUC) windows with the plots visible. But running using Mux; WebIO.webio_serve(req -> ui) afterwards still left me with an empty (forever loading) page when visiting localhost:8000.

Any ideas if there are better warm-up plot commands I should have run instead? Otherwise, if this might be a bug in Mux or WebIO, how do you suggest I describe it, and on which of the repos should I open it?

Actually, this is probably my bad! Somehow I think that you also need to specify on what “page” of the website you want to run the app, so to have it at http://localhost:8000 you need to say:

WebIO.webio_serve(page("/", req -> ui))

whereas for example WebIO.webio_serve(page("/test", req -> ui)) would serve it at http://localhost:8000/test. This is useful when you want to serve multiple pages, in which case you can “stack” the two apps:

app1 = page("/", req -> ui1)
app2 = page("/test", req -> ui2)
app = Mux.stack(app1, app2)

I’m not sure why omitting this worked at some point on my machine. I’ll edit the original post.

All right! Using WebIO.webio_serve(page("/", req -> ui)) does work now :slight_smile:

I’m glad testing in another machine helped identify issues.

Now that this hopefully works reliably as plug-and-play snippet, I would suggest adding this code to the README, below the screenshot that is there currently, as that provides a way to immediately try out the example and get a taste of what working with the package is like. (The image may need to be slightly adjusted – IIUC it’s lacking the legend positioning controls – or the code could be tweaked to match it.)

While you’re at it, please add a semicolon at the end of the ui = myplot(...) line, otherwise its output gets spilled into the console.

By the way, wouldn’t it make sense for WebIO.webio_serve(req -> ui) to be automatically equivalent to WebIO.webio_serve(page("/", req -> ui))? That sounds like a reasonable default to me. If you agree, I can open an issue in the WebIO repo.

True, that’s very annoying as the text output can be massive…

Definitely open an issue! May be a bit tricky to get it to work due to how it works internally but we should at least discuss it properly on GitHub

Ok! Opened as issue #189.