Black Lives Matter. Please consider donating to Black Girls Code today.
Dash HoloViews is now available! Check out the docs.

Bidirectional component synchronization

A few years ago, when i started using Dash, I wanted to do bidirectional component synchronization. For me, the need has arisen multiple times since then, but I never found a good solution.

To keep the functionality compatible with the standard Dash library, I have encapsulated it in a Sync component, which has the ability to synchronize properties of it’s descendants. As an example, consider the follow code,

import dash
import dash_core_components as dcc
from dash_extensions import Sync

app = dash.Dash()
app.layout = Sync([dcc.Input(id="input", value=50), dcc.Slider(id="slider", min=0, max=100, value=50)],
                  circles=[[dict(id="input", prop="value", get="parseFloat"), dict(id="slider", prop="value")]])

if __name__ == '__main__':
    app.run_server()

which keeps a slider and a input in sync (on the client side, without using callbacks),

Peek 2020-11-10 13-46

In the current syntax, the synchronization is specified as a list of dependency circles. Each circle is a list of dicts holding (component_id, component_property) specifying what properties to target. In addition a get (used to parse a value before it’s sent to other components) and/or set (used to transform a value before it’s set) function can be passed.

Does this syntax make sense? Do you have any ideas for improvement(s)? :slight_smile:

3 Likes

Hi @Emil,

For bidirectional sync, I would rather prefer the possibility to have a callback accepting both fields as inputs and outputs like in the code below.

import dash_core_components as dcc
import dash_html_components as html
from dash import Dash, callback_context, no_update
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate

app = Dash(__name__)

app.layout = html.Div([dcc.Input(id="deg-fahrenheit"), dcc.Input(id="deg-celsius")])


@app.callback(
    [Output("deg-fahrenheit", "value"), Output("deg-celsius", "value")],
    [Input("deg-fahrenheit", "value"), Input("deg-celsius", "value")],
)
def sync_inputs(fahrenheit, celsius):
    ctx = callback_context

    if not ctx.triggered:
        raise PreventUpdate

    if ctx.triggered[0]["prop_id"] == "deg-fahrenheit.value":
        # deg-fahrenheit given, update deg-celsius
        return no_update, (fahrenheit - 32) * 5 / 9
    elif ctx.triggered[0]["prop_id"] == "deg-celsius.value":
        return celsius * 9 / 5 + 32, no_update

    raise ValueError("should never happen")

if __name__ == "__main__":
    app.run_server(debug=True)

This code will not run today as it will complain in the browser about:

Same Input and Output

This is probably to avoid loop/cycles in the dependencies. If it is just that, then maybe a light change in the Dash logic to ensure that the return of a callback cannot retrigger itself (ie simple way to avoid infinite callback loop due to a loop on a component property (https://en.wikipedia.org/wiki/Loop_(graph_theory)) would be sufficient to allow syncing of components in a generic way.

Moreover, if for each change on the UI, the timestamp of the change would be saved and transmitted to the callback (via the callback_context), it could be possible to sync 3 (or more) components together by looking at the last 2 changed components and recalculating the 3rd (or similar logic).

The syntax your propose is very coherent with the Dash echo system, but I am not sure that i can be implemented without changes to the internals of Dash (or a huge amount of hacks); that’s why i aimed for the component for starters. What would you said, @chriddyp?

One thing i guess that could be done would be to create a component that simply monitors the state of it’s children, but doesn’t do any updates; the updates would be handled by Dash. It would enable a syntax more similar to what you have suggested. Hence something like (the below is just pseudo code to illustrate what i mean),

import json
import dash_core_components as dcc
import dash_html_components as html

from dash import Dash, no_update
from dash.dependencies import Input, Output
from dash_extensions import Monitor

app = Dash(__name__)
app.layout = html.Div(
    Monitor([dcc.Input(id="deg-fahrenheit"), dcc.Input(id="deg-celsius")],
            monitors=dict(deg=["deg-fahrenheit.value", "deg-celsius.value"]))
)

@app.callback(
    [Output("deg-fahrenheit", "value"), Output("deg-celsius", "value")],
    [Input("monitor", "data")],
)
def sync_inputs(data):
    monitor = json.loads(data)["deg"]

    if monitor["trigger"] == "deg-fahrenheit.value":
        return no_update, (monitor["deg-fahrenheit.value"] - 32) * 5 / 9
    elif monitor["trigger"] == "deg-celsius.value":
        return monitor["deg-celsius.value"] * 9 / 5 + 32, no_update

    raise ValueError("should never happen")

if __name__ == "__main__":
    app.run_server(debug=True)

Indeed, this would need a change in the dash logic on client side (maybe just not triggering the error) but would allow much more flexibility (and less cognitive load for the developer).

@chriddyp , do you think it is doable?
And about adding some extra information about timestamps in the callback context?

I finally got around to implementing the first version of the Monitor component. The example now looks like this,

import dash_core_components as dcc
import dash_html_components as html
from dash import Dash, no_update
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
from dash_extensions import Monitor

app = Dash()
app.layout = html.Div(Monitor([
    dcc.Input(id="deg-fahrenheit", autoComplete="off", type="number"),
    dcc.Input(id="deg-celsius", autoComplete="off", type="number")],
    probes=dict(deg=[dict(id="deg-fahrenheit", prop="value"), 
                     dict(id="deg-celsius", prop="value")]), id="monitor")
)

@app.callback([Output("deg-fahrenheit", "value"), Output("deg-celsius", "value")], 
              [Input("monitor", "data")])
def sync_inputs(data):
    # Get value and trigger id from monitor.
    try:
        probe = data["deg"]
        trigger_id, value = probe["trigger"]["id"], float(probe["value"])
    except (TypeError, KeyError):
        raise PreventUpdate
    # Do the appropriate update.
    if trigger_id == "deg-fahrenheit":
        return no_update, (value - 32) * 5 / 9
    elif trigger_id == "deg-celsius":
        return value * 9 / 5 + 32, no_update


if __name__ == '__main__':
    app.run_server()

After playing around with the Sync and Monitor components, I have found that I like the monitoring approach better. Since the update is handled in Dash, it gives you much more flexibility as compared to the sync approach. If you wan’t to try it out, it’s available in the latest RC version of dash-extensions,

2 Likes

Very nice @emil! Is it possible for these elements to live in different parts of the DOM? ie not to be siblings.

Thanks! Yes, the children are iterated recursively (i haven’t done extensive testing yet, though). The only limitation is that the Monitor only sees its own children.

Hi Emil,
Congratulation for your exelent work!!!
Is all that extentions shown on “Show and Tell” section?
because the title "Bidirectional … " doesn’t represent all the beautiful new option you provide here. :slightly_smiling_face:

Could the limit about using the same property in Input and Output be lifted in Dash? This would make life easier than using a custom component.

Thanks! I think most of the extensions have been mentioned i the forum, some of them in show & tell. But there is no “complete thread” :slight_smile:

1 Like