Julia Community 🟣

Cover image for Make beautiful web apps in Julia with... Genie Builder
Mathieu Aucejo
Mathieu Aucejo

Posted on • Updated on

Make beautiful web apps in Julia with... Genie Builder

In a previous post, I tried to explain how to use Dash.jl to develop a web app to illustrate the vibration behavior of undamped/damped mass-spring oscillators.

Today, my goal is to develop the same application, but with Genie Builder, a VSCode plugin that aims to simplify the creation of the user interface by dragging and dropping interface elements. I have to admit that this is a very exciting proposition, especially for people not well versed in HTML and CSS.

Table Of Contents

  1. First steps
  2. Goal of the app
  3. Let's implement the app!
  4. Final thoughts

1. First steps

As for any framework, one of the best practice is to read the documentation. The natural entry point is the documentation section of the web site dedicated to the Genie Framework.

Genie Framework Documentation section

Genie Framework Documentation section

As you can see on the previous picture, the Genie framework is built around five modules (Genie, Stipple, SearchLight, StippleUI and StipplePlotly). At the time of writing this post (January 2023), you have to keep in mind that the documentation is still under development, meaning that all the information needed for building the present app are lacking. However, this is compensated by the active and and friendly Genie community and the existence of a discord channel (special thanks to @abhimanyuaryan for guiding my first steps with the Genie Builder). Finally, the best way I found to familiarize with the Genie Builder are the tutorial written by @pgimenez that you can find here and there.

2. Goal of the app

The app we want to develop is a 1:1 translation of the app I have previously implemented using Dash.jl and presented in the figure below.

Target application developed using Dash.jl

Target application developed using Dash.jl

As shown in the previous picture is composed of:

  • A header
  • A plotting zone
  • A zone containing four sliders whose titles display the current value of each slider

In the rest of this tutorial, I will try to show how easy it is to create an application using Genie Builder.

3. Let's implement the app!

Before entering in the core of this tutorial, it is important to briefly explain how to create an app with Genie Builder. I suppose here that you have already install the plugin in VSCode. That being said, we can divide the process into 4 steps:

3.1. Start the server

You have to click on Start Server in the Genie Builder Server panel. This step is mandatory to open the no-code interface for building the UI.

Start the server

Click on Start Server

On my machine (Dell Latitude 7410 - Intel Core i7-10610U @1.8 GHz (8 CPUs) - 32 Gb RAM), the server starts in 1min 45s on average.
Disclaimer: The elapsed time is the average of five measurements carried out by the chronometer of my cell phone!

3.2. Create a new app

To this end, you just have to click on + in the Genie Builder Apps panel

App creation

Click on + to create a new app

By default, this creates a basic app structure. One has to note that the app is actually a Julia project, which is saved in the /.julia/geniebuilder/apps folder. I have to admit that I would prefer to save the app in the folder of my choice. I hope it will be possible in a near future (but it is not very important. It just more convenient IMHO). At the very beginning, the apps folder contains four files:

  • app.jl containing the analysis code and the app's logic.
  • app.jl.html containing the HTML components defining the UI
  • Project.toml and Manifest.toml

On my machine, starting the app takes 1min on average. This timing include the loading of the app (detection of app.jl.html, bindings, ...) and the no-code interface (and many other things).

The code of the generic app is as follows:

  • for app.jl
module App

using GenieFramework
@genietools

@app begin
  @out message = "Hello World!"

  @onchange isready begin
    @show "App is loaded"
  end
end

@page("/", "app.jl.html")

end
Enter fullscreen mode Exit fullscreen mode
  • for app.jl.html
<h1>{{message}}</h1>
<p>This is the default view for the application.</p>
<p>You can change this view by editing the file <code>app.jl.html</code>.</p>
Enter fullscreen mode Exit fullscreen mode

Some comments can be made here:

  • A Genie app is defined as a Julia module and not a script. So, it can't be launch directly using julia MyApp.jl. See @pgimenez tutorial for instance.
  • The macro @genietools can be seen as a shortcut for using Genie, Stipple, StippleUI, StipplePlotly. This macro performs other operations. For more information, see GenieFramework.jl Github repo.
  • Any text of the app can modified programmatically. To do this, a variable mytext must be defined in the app.jl using @out mytext = "My custom text". Then, to make this text appear when the app is launched in a web browser, a reference to the variable mytext must be done in app.jl.html using {{mytext}}.
  • The macro @page generates the HTML code corresponding to a single page application. See Stipple.jl Documentation for details.

To start with a fresh app, you can erase the content of app.jl.html, while keeping the structure of the app.jl file, that is:

module App

using GenieFramework
@genietools

# Put/include the analysis code here

@app begin
  # Define the inputs and the outputs here

  @onchange isready begin
    # Define the interactivity here
  end
end

@page("/", "app.jl.html")

end
Enter fullscreen mode Exit fullscreen mode

3.3. Create the analysis code

The analysis code is exactly the same as that explained in my post on Dash.jl. It basically contains two functions:

  • response that computes the free response of the mass of a mass-spring system for a given frequency f0f_0 , damping ratio ξ\xi and in displacement and velocity initial conditions, x0x_0 and v0v_0 .
  • damping that defines the mapping between a discrete set of damping factors and the values of the slider will create later. This function allows to keep constant the gap between each value of the slider.

For the sake of completeness, the analysis code is recalled below:

const t = 0.:1e-3:2.

function response(f₀ = 10., ξ = 0.01, x₀ = 1., v₀ = 1.)
    ω₀ = 2π*f₀     
    if 0. ≤ ξ < 1. 
       Ω₀ = ω₀*√(1. - ξ^2)
      x = (x₀*cos.(Ω₀*t) + (v₀ + ξ*ω₀*x₀)*sin.(Ω₀*t)/Ω₀).*exp.(-ξ*ω₀*t)
    elseif ξ == 1.
      x = @. (x₀ + ω₀*x₀*t + v₀*t)*exp(-ω₀*t)
    else
      β = ω₀*√(ξ^2 - 1.)
      x = @. (x₀*cosh(β*t) + (v₀ + ξ*ω₀*x₀*sinh(β*t))/β)*exp(-ξ*ω₀*t)
    end
end

function damping(value)
    if value == 1
        return 0.
    elseif value == 2
        return 0.001
    elseif value == 3
        return 0.01
    elseif value == 4
        return 0.1
    elseif value == 5
        return 1.
    else
        return 1.5
    end
end
Enter fullscreen mode Exit fullscreen mode

3.4. Create the UI using the no-code editor

The no-code editor allows defining the UI by dragging and dropping interface elements. For our app, the UI looks like the following picture.

UI

View of the no-code editor

To create the interface, we have created:

  • A h1 environment containing the title of the app using Content -> H1. The latter will be programmatically defined through the variable title.
  • Two columns using the Layout -> Sidebar right element of the no-code editor. In the left column, a line plot has been inserted using the Charts -> Line element of the editor. The right column requires more work, since it contains 4 sliders, each of them having a title. Each slider is created using the following procedure:

    a. Use Content -> Title + Content. This creates a h6 environment to define the title of the slider. This title must be defined programmatically if one wants to display the current value of the slider in the corresponding title. Here, the titles are created from the variables freqtitle, xititle, x0title and v0title.

    b. Use Forms -> Slider to create a slider. Its complete definition can be made by clicking on the slider and filling its properties. For instance, one has for the slider related to the value of the natural frequency of the mass-spring system:

Slider properties

Definition of the slider properties

Finally, the decoration of the right column can be set by selecting the column, clicking on Open Style Manager (i.e. click on the pencil in the editor) and filling the Decorations section (see Figure below).

Column Decoration

Column decoration

Thanks to the no-code editor, one obtains the following app.jl.html file:

<h1 id="iqml">{{title}}</h1>
<div class="row">
    <div class="col">
        <plotly :data="[{
      x: [2, 3, 4, 5],
      y: [16, 5, 11, 9],
      mode: 'lines',
      type: 'scatter'
    }]"></plotly>
    </div>
    <div class="col col-3">
        <div class="row">
            <div class="col col-12">
                <h6>{{freqtitle}}</h6>
                <q-slider :min="1." :max="50." :step="1." :marker-labels="{1.:'1', 10.:'10', 20.:'20', 30.:'30', 40.:'40', 50:'50'}"></q-slider>
            </div>
        </div>
        <div class="row">
            <div class="col col-12">
                <h6>{{xititle}}</h6>
                <q-slider :min="1" :max="6" :step="1" :marker-labels="{1:'0', 2:'0.001', 3:'0.01', 4:'0.1', 5:'1', 6:'1.5'}"></q-slider>
            </div>
        </div>
        <div class="row">
            <div class="col col-12">
                <h6>{{x0title}}</h6>
                <q-slider :min="-1." :max="1." :step="0.1" :marker-labels="{'-1':'-1', '-0.5':'-0.5', '0':'0', '0.5':'0.5', '1':'1'}"></q-slider>
            </div>
        </div>
        <div class="row">
            <div class="col col-12">
                <h6>{{v0title}}</h6>
                <q-slider :min="-100." :max="100." :step="1." :marker-labels="{'-100':'-100', '-50':'-50', '0':'0', '50':'50', '100':'100'}"></q-slider>
            </div>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

As you can see in the code above:

  • The plotting interface is Plotly thanks to StipplePlotly.jl. By default, the editor creates a dummy line plot.
  • Under the hood, Genie Builder uses Quasar components under the hood (q-slider). This is interesting, since you can have a look on the Quasar website to learn how to use a particular components of the no-code editor. This avoid the Genie team duplicating the documentation. Nice idea!

To finish the app, it remains to connect the UI elements to the analysis code.

3.5 Make the app interactive!

To make the app interactive, one has to define its inputs and its outputs. It is easily done thanks to the @in and @out macros. To summarize a little bit, the inputs are the values of the sliders. Here, they are named f0, xi, x0, v0, while the outputs are the title of the app title, the titles of the sliders freqtitle, xititle, x0title, v0title and the plot plotdata and its layout plotdata_layout. The code corresponding to the initialization of this variable is:

  @in f0 = 10.
  @in xi = 3
  @in x0 = 0.5
  @in v0 = 0.

  @out title = "Free response of a mass-spring system"
  @out freqtitle = ""
  @out xititle = ""
  @out x0title = ""
  @out v0title = ""

  @out plotdata_layout = PlotLayout(
    xaxis = [PlotLayoutAxis(title_text = "Time (s)",title_font = Font(20), tickfont = Font(14))],
    yaxis = [PlotLayoutAxis(xy = "y", title_text = "Displacement (m)", ticks = "outside", ticklen = 10, tickcolor = "white", title_font = Font(20), tickfont = Font(14))]
  )
  @out plotdata = PlotData()
Enter fullscreen mode Exit fullscreen mode

As a side note, the documentation of PlotLayoutAxis can found here. At the time of writing this tutorial (January 2023), the function PlotLayout was not documented. However, its basic usage can be found by exploring the example apps provided by the Genie Team.

At the stage, we have to implement the callback that allows to update the plot when the value of one the sliders changes. It is basically done thanks to the macro @onchangeany, that monitor any change in the input variables f0, xi, x0, v0. In the present case, the callback is implemented as follows:

@onchange isready, f0, xi, x0, v0 begin
    freqtitle = "Resonance frequency: $(Int(f0)) Hz"
    xititle = "Damping ratio: $(damping(xi))"
    x0title = "Initial displacement: $(round(x0, digits=1)) m"
    v0title = "Initial velocity: $(Int(v0)) m/s"

    rep = response(f0, damping(xi), x0, v0)
    plotdata = PlotData(
      x = t,
      y = rep,
      mode = "lines"
    )
Enter fullscreen mode Exit fullscreen mode

It should be noted here that the function PlotData is not currently documented. However, it seems to follow a syntax similar to PlotlyJS.jl.

To complete the interactivity part of this tutorial, we have to assign the value of each slider to the corresponding variable. To do this, we have to return to the no-code interface, select each slider element and fill the Data Binding item. For instance, to assign the value of the slider to f0, we have:

Data Binding

Data Binding for f0

For connecting the plot area to plotdata and plotdata_layout, we have to click of the plot element and fill the Data and Layout items as follows:

Plot Data

Connection of the plot are to plotdata and plotdata_layout

These operations are translated into code in the following way:

# For the plot area
<div class="col">
        <plotly :data="plotdata" :layout="plotdata_layout"></plotly>
</div>

# For sliders
<q-slider v-model="f0" :min="1." :max="50." :step="1." :marker-labels="{1.:'1', 10.:'10', 20.:'20', 30.:'30', 40.:'40', 50:'50'}"></q-slider>
Enter fullscreen mode Exit fullscreen mode

3.6. Let's finish the app

To finish our app, it remains to define the title appearing in the tab of the web browser. How to do this with Genie builder is no documented. However, the answer can be found on the discord channel of the Genie Framework. This done by defining our title as a constant string and by adding it as a parameter of the macro @page that generates the HTML code of the app. In more explicit terms, this code in the following way:

const apptitle = "Free response of a mass-spring system"
@page("/", "app.jl.html", Stipple.ReactiveTools.DEFAULT_LAYOUT(title = apptitle))
Enter fullscreen mode Exit fullscreen mode

As indicated by @pgimenez in his post, the standard way to run the app outside of Genie Builder consists in including Server.isrunning() || Server.up(8050) (where 8050 is the port number) at the end of app.jl. Then, to launch the app, one just has to open the REPL with julia --project in the app's folder and type include("app.jl").

4. Final thoughts

Genie Builder is a fantastic tool to create an application quickly and easily. From my limited experience with Dash and Genie, both frameworks seem to be on par in terms of functionality. So the choice of framework is mainly a matter of taste. The no-code interface is a really cool feature, especially for people who are not well versed in html or ill at ease when building a UI programmatically. I think the no-code interface is a must-have feature that is not currently available for Dash.jl.

That being said, an another point of comparison is the starting time of the applications. For the present, the starting times obtains from a fresh julia session are:

  • 2min 20s for the Genie app
  • 50s for the Dash app Except this noticeable difference, the user experience is similar.

In conclusion, there is no good choice. Genie and Dash are two really good frameworks. They have both their own components library (included in StippleUI for Genie) or can use external library such as Quasar for Genie and Bootstrap for Dash.jl through DashBootstrapComponents.jl. For plotting, both framework use Plotly as a backend. I strongly encourage you to use both!

Top comments (5)

Collapse
 
abhimanyuaryan profile image
abhimanyuaryan

Awesome post Mathieu. Side note you can use PackageCompiler to bring startup time to seconds for GenieApp: github.com/GenieFramework/Dockeriz...

Example deployed here on heroku: pkgs.genieframework.com/ :)

Collapse
 
hhaensel profile image
hhaensel

Great article! Let me add two points that have changed lately:

  • @handlers does still work but the latest recommended workflow is to use @app
  • @onchangeany is deprecated, instead use @onchange for any number of arguments

Here's a short demo app using StippleUI to define a primitive UI

using GenieFramework
@genietools

@app begin
  # Define the inputs and the outputs here
  @in x1 = "Hello"
  @in x2 = "world"
  @out y = "still undefined"

  @onchange isready begin
    # Define the initial calculations here
    # ...

    # trigger x1-handler
    notify(x1)
  end

  @onchange x1, x2 begin
    # update variable y
    y = "$x1 $(x2)!"

    # display a notification message
    # !!! not to be confused with `notify(var)`
    @notify "x1 ('$x1') or x2 ('$x2') has changed!"
  end

end

function ui()
  row(cell(class = "st-module", [
    textfield("Input 1", :x1)
    textfield("Input 2", :x2)
    row(cell("{{y}}"))
  ]))
end

@page("/", ui)
up()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
maucejo profile image
Mathieu Aucejo

Thanks @hhaensel for pointing this. I have edited the post to be in phase with the new API.

Collapse
 
abhimanyuaryan profile image
abhimanyuaryan

Another demo: huggingface.co/spaces/nooji/ImpCat...

build by Anuj which uses PackageCompiler.jl and GenieFramework

Collapse
 
maucejo profile image
Mathieu Aucejo