How to use JSServe + WGLMakie

One can use JSServe and WGLMakie in Pluto, IJulia, Webpages - and Documenter! It's possible to create interactive apps and dashboards, serve them on live webpages, or export them to static HTML.

This tutorial will run through the different modes and what kind of limitations to expect.

First, one should use the new Page mode for anything that displays multiple outputs, like Pluto/IJulia/Documenter. This creates a single entry point, to connect to the Julia process and load dependencies. For Documenter, the page needs to be set to exportable=true, offline=true. Exportable has the effect of inlining all data & js dependencies, so that everything can be loaded in a single HTML object. offline=true will make the Page not even try to connect to a running Julia process, which makes sense for the kind of static export we do in Documenter.

using JSServe
Page(exportable=true, offline=true)

After the page got displayed by the frontend, we can start with creating plots and JSServe Apps:

using WGLMakie
# Set the default resolution to something that fits the Documenter theme
set_theme!(resolution=(800, 400))
scatter(1:4, color=1:4)

As you can see, the output is completely static, because we don't have a running Julia server, as it would be the case with e.g. Pluto. To make the plot interactive, we will need to write more parts of WGLMakie in JS, which is an ongoing effort. As you can see, the interactivity already keeps working for 3D:

N = 60
function xy_data(x, y)
    r = sqrt(x^2 + y^2)
    r == 0.0 ? 1f0 : (sin(r)/r)
l = range(-10, stop = 10, length = N)
z = Float32[xy_data(x, y) for x in l, y in l]
    -1..1, -1..1, z,
    colormap = :Spectral

There are a couple of ways to keep interacting with Plots in a static export.

Record a statemap

JSServe allows to record a statemap for all widgets, that satisfy the following interface:

# must be true to be found inside the DOM
is_widget(x) = true
# Updating the widget isn't dependant on any other state (only thing supported right now)
is_independant(x) = true
# The values a widget can iterate
function value_range end
# updating the widget with a certain value (usually an observable)
function update_value!(x, value) end

Currently, only sliders overload the interface:

using Observables

App() do session::Session
    n = 10
    index_slider = Slider(1:n)
    volume = rand(n, n, n)
    slice = map(index_slider) do idx
        return volume[:, :, idx]
    fig = Figure()
    ax, cplot = contour(fig[1, 1], volume)
    rectplot = linesegments!(ax, Rect(-1, -1, 12, 12), linewidth=2, color=:red)
    on(index_slider) do idx
        translate!(rectplot, 0,0,idx)
    heatmap(fig[1, 2], slice)
    slider = DOM.div("z-index: ", index_slider, index_slider.value)
    return JSServe.record_states(session, DOM.div(slider, fig))
z-index: 1