GUI Development

Basic component structure

All UI components can be found in the nqontrol.gui package, smaller parts in the widgets folder. The first entry point which wraps all other components is ui.py. Each component will implement two features by default: a layout property and a setCallbacks() method. The layouts can be nested within each other, just as one would using normal HTML5/CSS. Generally, the layouts have to be created first and the callback method invoked afterwards. So each component first adds all its subcomponent layouts and then calls their setCallbacks method as part of its own.[^1] We’ll have a more indepth look at this below.

[^1]: The most recent version of dash allows for pattern matching callbacks, thus enabling the adding of UI widgets at runtime. This feature will be implemented in a future version.

Bootstrap

For layouting the GUI we use Bootstrap 4. Bootstrap let’s one formulate a layout in terms of columns and rows. Please beware: Cols can only be children of Rows, so if you wanted to implement a Col within a Col, you would first have to embed another Row. (It will still work but might lead to some buggy positioning etc.) Bootstrap assigns a fixed number of 12 columns to a container, so think about it in terms of x of 12 columns.

You can responsively assign these via the className attributes of components, e.g. passing col-2 col-sm-3 col-xl-6 to a container would make it fill 2 columns on the smallest screen (mobile), then scale up to 3 columns on small screens and only once it hits xl it would fill 6 columns.

Bootstrap defines a couple of these tags (off the top of my hat xs, sm, md, lg, xl - where xs is the default and does not have to be explicitly stated). The full bootstrap documentation can be found on https://getbootstrap.com/ and we encourage you to read it if you’d like to add custom components to the GUI.

Layout & callback basics

Let’s examine a few of the inner workings of the Dash UI using an old RampWidget. Please note that the layout below might not be up-to-date with the UI components. It still describes the inner workings pretty well though. The layout is organized as follows:

# widgets implement a layout either as a module variable
layout = html.Div(children=[...], ...)
# or in some cases, if the widget has object structure, as a property (comment v1.3: this might be due to changes in the coming releases)
@property
def layout(self):
    """Return the elements' structure to be passed to a Dash style layout, usually with html.Div() as a top level container. For additional information read the Dash documentation at https://dash.plot.ly/.

    Returns
    -------
    html.Div
        The html/dash layout.

    """
    # dash implements most standard html Components, e.g. the Detail
    # the Details itself is assigned a `col-12` tag, see at the end
    # within that, two sliders are implemented in `row` containers, which contain a label for each slider
    # also note that we can assign other bootstrap flex properties like `justify-content-*` in the `className` attribute
    # these will handle the alignment of the components
    return html.Details(
        children=[
            # Ramp title and current ramp
            html.Summary(
              # more code...
            ),
            # Amplitude label and slider
            html.Div(
                children=[
                    html.P("Amplitude (V)", className="col-12"),
                    dcc.Slider(
                        id=f"ramp_amp_slider_{self._servoNumber}",
                        # more code...
                        className="col-10",
                        updatemode="drag",
                    ),
                ],
                className="row justify-content-center",
            ),
            # Frequency label and slider
            html.Div(
                children=[
                    html.P("Frequency (Hz)", className="col-12"),
                    dcc.Slider(
                        id=f"ramp_freq_slider_{self._servoNumber}",
                        # more code...
                        className="col-10",
                        updatemode="drag",
                    ),
                ],
                className="row justify-content-center",
            ),
        ],
        className="col-12 d-inline mt-1 mr-2",
        style={
            "background-color": "#f2f4f5",
            # ...
        },
    )

Now, if you’ve read this carefully, it should give you at least a rough concept off how these layouts look. We recommend you also check out the dash documentation. The second important feature of these UI widgets are callbacks, so let’s check out a callback[^1] of the AutolockWidget.

# widgets define a setCallbacks method
def setCallbacks(self):
    """Initialize all callbacks for the given element."""
    # the app object is imported as a dependency at the start of most modules
    # Callbacks define three things: Outputs, Inputs and States
    # Usually, a callback has only one output, though Dash also allows for multi-output callbacks
    # The inputs are the "triggers" of the callback (only one trigger will fire for each callback, the other one will be passed its previous value)
    # but if we want to pass along more values, we can use State('id', 'field')

            # Slider callbacks
    app.callback(
        Output(f"current_lock_ramp_{self._servoNumber}", "children"),  # the second parameter is the "target" value of the dash component
        [
            Input(f"lock_amplitude_slider_{self._servoNumber}", "value"),
            Input(f"lock_offset_slider_{self._servoNumber}", "value"),
            Input(f"lock_frequency_slider_{self._servoNumber}", "value"),
        ],
        # There could also be a  [State('some_id', 'value')]
    )(self._lockRampCallback)  # this is the function which will be fired, whenever the callback triggers, it is defined below


    # Callback for the Lock widget's amplitude, offset and frequency slider
    # Has to have three parameters (apart from self) because the callback will always call with three values (from three inputs)
    # The parameters are: "number of inputs + number of states"
    # callback functions need to return something, every callback needs an output component (even if it is a hidden Storage component)
    # If you use multiple outputs, return a list with values in the correct order
def _lockRampCallback(self, amplitude, offset, frequency):
    ctxt = callback_context  # in some cases, the callback context is useful, it contains information of which Input fired the callback
    return _callbacks.callLockRamp(  # we implement the callbacks in separate modules (same folder as the widgets)
        amplitude, offset, frequency, ctxt, self._servoNumber
    )

# btw, the standard way of creating callbacks would be with decorators
@app.callback(
    Output(...),
    [Input(...)]
)
def some_callback(input1):
    return someoutput
# without the pattern implementation that would be a lot of copy paste work, e.g. when defining the callbacks for 5 filter modules

This might be a little confusing and the exact implementation might change (also see footnote), but generally this principle holds:

  • layout: components with some ID (dict or string) and some field containing a value (children, value, n_clicks…)

  • callbacks: a relation which needs and INPUT (id+value field) as trigger, an OUTPUT (id+value field) as target (sometimes also a STATE if you’d like to use a value from somewhere else in the UI as part of the callback function) and the CALLBACK function itself Again, please note that only the INPUT which fires the callback will have the most recent value. Naturally, the user cannot change two values at the same time, that would hurt Einstein. If you’d like to determine which input fired, use the callback_context (from dash import callback_context) inside of a callback function.

[^1]: The variant used here is not the standard way recommended in the dash docs. This was due to old dash not supporting callback patterns, meaning you’d have to declare all callbacks beforehand for specific component IDs. With the new dash features we will implement callbacks differently in the future, so take this documentation with some caveats.