Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

Description

@chriddyp

A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

At this point, I don't have any concrete recommendations, I am just sharing my explorations.


Persisting values across tabs

Here's a classic example: an app with two tabs.

The first tab contains all of the controls.

The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

The code below does not work but it is how users would intuitively write an example like this.
That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

In summary, there are two challenges to this:

  1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
  2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

This is how users are currently writing these applications (this example does not work):

app.layout=html.Div([
dcc.Tabs(
id='tabs',
tabs=[
dcc.Tab(label='Controls', value='controls'),
dcc.Tab(label='Model', value='model'),
],
value='controls'
),
html.Div(id='content')
])
@app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
ifselected_tab=='controls':
returnhtml.Div([
dcc.Dropdown(
id='model',
# ...
),
dcc.Input(
id='velocity'# ...
)
])
elifselected_tab=='model':
returnhtml.Div([
dcc.Graph(
id='graph-1'
),
])
@app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
# ...

In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


One option might be "two-way synced" components. That is, components that have the same
value and update each other whenever either one of them changes.

With synced components, the Dash developer could sync the controls with a global store component:

Here's what this might look like

app.layout=html.Div([
dcc.Store(id='store_model'),
dcc.Store(id='store_velocity'),
dcc.Tabs(
id='tabs',
tabs=[
dcc.Tab(label='Controls', value='controls'),
dcc.Tab(label='Model', value='model'),
],
value='controls'
),
html.Div(id='content')
])
@app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
ifselected_tab=='controls':
returnhtml.Div([
dcc.Dropdown(
id='model',
value='mtl'# ...
),
dcc.Input(
id='velocity'# ...
)
])
elifselected_tab=='model':
returnhtml.Div([
dcc.Graph(
id='graph-1'
),
])
@app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
# ...# <-- New line - some type of sync APIapp.sync(
Sync('store_model', 'data'),
Sync('model', 'value')
)
app.sync(
Sync('store_velocity', 'data'),
Sync('velocity', 'value')
)

One challenge with this approach is the initialization logic could be ambiguous.
Here's the lifecycle in this app:

  1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
  2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
  3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
  4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

app.sync(
Sync(id='store_model', property='data', default=None),
Sync('model', 'value', default='mtl')
)

That's not great though, because then the default can't be conditional.

Alternatively, the Sync property could be something provided in the layout. A new special property:

@app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
ifselected_tab=='controls':
returnhtml.Div([
dcc.Dropdown(
id='model',
value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
target_property='data',
default='mtl'# <-- This could be dynamic
)
# ...
),
dcc.Input(
id='velocity'value=dash.properties.Synced(
target_id='store_velocity',
target_property='data',
default=''
)
)
])
elifselected_tab=='model':
returnhtml.Div([
dcc.Graph(
id='graph-1'
),
])

In this case, the logic would be:

  1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
    In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
  2. If the synced component's "target"'s value is defined, then use that instead.
    In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

Alternatively, the default value could be pushed into the dcc.Store:

app.layout=html.Div([
dcc.Store(id='store_model', data='mtl'),
dcc.Store(id='store_velocity', data=0),
# tabs & content ...
])
@app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
ifselected_tab=='controls':
returnhtml.Div([
dcc.Dropdown(
id='model',
value=dash.properties.Synced(
id='store_model',
property='data',
)
),
dcc.Input(
id='velocity'value=dash.properties.Synced(
id='store_velocity',
property='data'
)
)
])
elifselected_tab=='model':
returnhtml.Div([
dcc.Graph(
id='graph-1'
),
])

But in this model, the property value couldn't be dynamic.


Once potentially nice feature of using dcc.Store for this is the ability to save values
into localstorage across page loads. Lifecycle:

  1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
  2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
  3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
  4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

Other Synced applications

Two-way sync has come up in a few other cases:

  1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
app.layout=html.Div([
dcc.Location(id='url'),
dcc.Tabs(
id='tabs',
value=dash.properties.Synced(
id='url',
property='href'
),
children=[
dcc.Tab(value='/page-1', label='Analysis 1'),
dcc.Tab(value='/page-2', label='Analysis 2'),
]
),
html.Div(id='output')
])
@app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
iftab=='/page-1':
# ...eliftab=='/page-1':
# ...
  1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
    image

  2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

    app.layout=html.Div([
    dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
    dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
    dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
    dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
    ])

    These could even be synced up with a dash_table.DataTable.selected_row_ids:

    app.layout=html.Div([
    dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
    dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
    dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
    dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
    dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
    ])

    Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

    app.layout=html.Div([
    dcc.Store(id='selected'),
    dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
    dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
    dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
    dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
    dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
    ])

    In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

    In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

    app.layout=html.Div([
    dcc.Store(id='selected', selectedids=union(
    dash.properties.Input('graph-1', 'selectedids'),
    dash.properties.Input('graph-2', 'selectedids'),
    dash.properties.Input('graph-3', 'selectedids'),
    dash.properties.Input('graph-4', 'selectedids'),
    dash.properties.Input('datatable', 'selected_row_ids'),
    )),
    dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
    dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
    dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
    dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
    dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
    ])

    (Transformations might bring up some other issues - exploring these below)

  3. The community has brought this up a few times:


Some other considerations

This is about as far as I've thought so far. Here are some other questions & ideas to explore.

  • Data-* properties?
    With this API, Synced only works with top-level properties.
    dcc.Store only has a single top-level property for storing data: data
    So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

    It'd be nice if you could specify arbitrary properties on a single dcc.Store:

    dcc.Store(id='store'),
    dcc.Dropdown(value=Synced('store', 'data-model')),
    dcc.Input(value=Synced('store', 'data-velocity')),

    We have special support for aria-* and data-* properties in components, so
    maybe we could use that?

  • Transformations?
    Through careful prop design across components, many properties could be 1-1 with each other.
    However, there might be some synced components that would require a (inverse)transformation.
    For example, two input components: one in lbs, the other in kgs.
    We might be able to just allow two callbacks for arbitrary transformations:

    app.layout=html.Div([
    dcc.Input(id='lbs'),
    dcc.Input(id='kg'),
    ])
    @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
    returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
    returnvalue/2.2

    Of course, if we supported these callback expressions then we wouldn't
    necessarily need the special layout property dash.properties.Synced, as
    a user could just write identity callbacks:

    app.layout=html.Div([
    dcc.Store(id='selected'),
    dcc.Graph(id='graph-1'),
    dcc.Graph(id='graph-2'),
    dcc.Graph(id='graph-3'),
    dcc.Graph(id='graph-4'),
    dash_table.DataTable(id='datatable')
    ])
    defidentity(i):
    returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
    app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
    app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
    app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
    app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
    app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
    app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
    app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
    app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
    app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

    And these could be written with clientside_callbacks too.

    The union/intersect version of crossfiltering would have a different cyclical transformation:

    app.layout=html.Div([
    dcc.Store(id='selected'),
    dcc.Graph(id='graph-1'),
    dcc.Graph(id='graph-2'),
    dcc.Graph(id='graph-3'),
    dcc.Graph(id='graph-4'),
    dash_table.DataTable(id='datatable')
    ])
    defintersect(*args):
    returnreduce(np.intersect1d, args)
    defidentity(i):
    returniapp.callback(
    Output('selected', 'data'),
    [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
    [Input('datatable', 'selected_row_ids')]
    )(intersect)
    app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
    app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
    app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
    app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
    app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

    So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

    Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

    app.layout=html.Div([
    dcc.Store(id='store_model'),
    dcc.Store(id='store_velocity'),
    dcc.Tabs(
    id='tabs',
    tabs=[
    dcc.Tab(label='Controls', value='controls'),
    dcc.Tab(label='Model', value='model'),
    ],
    value='controls'
    ),
    html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model'
    ),
    dcc.Input(
    id='velocity'
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])
    @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
    # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
    # ...?

    In this case, our cyclical callbacks aren't handling default values.

    Perhaps we need a notion of default values & undefined values.

    Then, our initialization routine would:

    1. Analyze the cyclical graph and look for default values & non-undefined values
    2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
    3. Firing everything in one complete circle:
    app.layout=html.Div([
    dcc.Store(id='store_model'),
    dcc.Store(id='store_velocity'),
    dcc.Tabs(
    id='tabs',
    tabs=[
    dcc.Tab(label='Controls', value='controls'),
    dcc.Tab(label='Model', value='model'),
    ],
    value='controls'
    ),
    html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model',
    value=dash.properties.Default('mtl') # <--- provide a default value
    ),
    dcc.Input(
    id='velocity'
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])
    @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
    returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
    returnstore_value

    Lifecycle:

    1. Page is loaded, store is undefined, tab 1 is selected

    2. Dropdown is rendered. Analyze the elements in the callback graph:

      • Store is undefined
      • Dropdown has a default value

      So, since dropdown has a default, so fire the dropdown->store callback

    3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

    4. Select the second tab.

    5. Select & render the first tab. Analyze the elements in the callback graph:

      • Store is defined
      • Dropdown has a default value

      In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

    This seems to work...

    It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

    In that case (where everything is defined), which callbacks do we fire on initialization?

    • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

    • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

      app.layout=html.Div([
      dcc.Store(id='selected'),
      dcc.Graph(id='graph-1', selected=['a', 'b']),
      dcc.Graph(id='graph-2', selected=['c', 'b']),
      dcc.Graph(id='graph-3', selected=['d', 'e']),
      dcc.Graph(id='graph-4', selected=['e', 'f']),
      dash_table.DataTable(id='datatable', selected=['a', 'g'])
      ])

      in which case, the system would evolve into:

      • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
      • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
      • Circle is complete.
    • If all of the values are defined at start, then it becomes ambiguous where to start

    app.layout=html.Div([
    dcc.Store(id='selected', selected=['a', 'b']),
    dcc.Graph(id='graph-1', selected=['a', 'b']),
    dcc.Graph(id='graph-2', selected=['c', 'b']),
    dcc.Graph(id='graph-3', selected=['d', 'e']),
    dcc.Graph(id='graph-4', selected=['e', 'f']),
    dash_table.DataTable(id='datatable', selected=['a', 'g'])
    ])

    If we start with Graphs->Store, then we get:

      1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
      1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

    But if we start Store->Graphs, then we get:

      1. store->graph-1, store->graph-2, ...: ['a', 'b']
      1. graphs->store: ['a', 'b']

    However, the user provided an "inconsistent" system, so perhaps this is OK.

    We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

    Description

    @chriddyp

    A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

    Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

    However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

    Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

    At this point, I don't have any concrete recommendations, I am just sharing my explorations.


    Persisting values across tabs

    Here's a classic example: an app with two tabs.

    The first tab contains all of the controls.

    The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

    The code below does not work but it is how users would intuitively write an example like this.
    That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

    In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

    This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
    Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

    In summary, there are two challenges to this:

    1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
    2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

    This is how users are currently writing these applications (this example does not work):

    app.layout=html.Div([
    dcc.Tabs(
    id='tabs',
    tabs=[
    dcc.Tab(label='Controls', value='controls'),
    dcc.Tab(label='Model', value='model'),
    ],
    value='controls'
    ),
    html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model',
    # ...
    ),
    dcc.Input(
    id='velocity'# ...
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])
    @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
    # ...

    In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


    One option might be "two-way synced" components. That is, components that have the same
    value and update each other whenever either one of them changes.

    With synced components, the Dash developer could sync the controls with a global store component:

    Here's what this might look like

    app.layout=html.Div([
    dcc.Store(id='store_model'),
    dcc.Store(id='store_velocity'),
    dcc.Tabs(
    id='tabs',
    tabs=[
    dcc.Tab(label='Controls', value='controls'),
    dcc.Tab(label='Model', value='model'),
    ],
    value='controls'
    ),
    html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model',
    value='mtl'# ...
    ),
    dcc.Input(
    id='velocity'# ...
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])
    @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
    # ...# <-- New line - some type of sync APIapp.sync(
    Sync('store_model', 'data'),
    Sync('model', 'value')
    )
    app.sync(
    Sync('store_velocity', 'data'),
    Sync('velocity', 'value')
    )

    One challenge with this approach is the initialization logic could be ambiguous.
    Here's the lifecycle in this app:

    1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
    2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
    3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
    4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

    In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

    app.sync(
    Sync(id='store_model', property='data', default=None),
    Sync('model', 'value', default='mtl')
    )

    That's not great though, because then the default can't be conditional.

    Alternatively, the Sync property could be something provided in the layout. A new special property:

    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model',
    value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
    target_property='data',
    default='mtl'# <-- This could be dynamic
    )
    # ...
    ),
    dcc.Input(
    id='velocity'value=dash.properties.Synced(
    target_id='store_velocity',
    target_property='data',
    default=''
    )
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])

    In this case, the logic would be:

    1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
      In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
    2. If the synced component's "target"'s value is defined, then use that instead.
      In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

    Alternatively, the default value could be pushed into the dcc.Store:

    app.layout=html.Div([
    dcc.Store(id='store_model', data='mtl'),
    dcc.Store(id='store_velocity', data=0),
    # tabs & content ...
    ])
    @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
    ifselected_tab=='controls':
    returnhtml.Div([
    dcc.Dropdown(
    id='model',
    value=dash.properties.Synced(
    id='store_model',
    property='data',
    )
    ),
    dcc.Input(
    id='velocity'value=dash.properties.Synced(
    id='store_velocity',
    property='data'
    )
    )
    ])
    elifselected_tab=='model':
    returnhtml.Div([
    dcc.Graph(
    id='graph-1'
    ),
    ])

    But in this model, the property value couldn't be dynamic.


    Once potentially nice feature of using dcc.Store for this is the ability to save values
    into localstorage across page loads. Lifecycle:

    1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
    2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
    3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
    4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

    Other Synced applications

    Two-way sync has come up in a few other cases:

    1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
    app.layout=html.Div([
    dcc.Location(id='url'),
    dcc.Tabs(
    id='tabs',
    value=dash.properties.Synced(
    id='url',
    property='href'
    ),
    children=[
    dcc.Tab(value='/page-1', label='Analysis 1'),
    dcc.Tab(value='/page-2', label='Analysis 2'),
    ]
    ),
    html.Div(id='output')
    ])
    @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
    iftab=='/page-1':
    # ...eliftab=='/page-1':
    # ...
    1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
      image

    2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

      app.layout=html.Div([
      dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
      dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
      dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
      dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
      ])

      These could even be synced up with a dash_table.DataTable.selected_row_ids:

      app.layout=html.Div([
      dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
      dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
      dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
      dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
      dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
      ])

      Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

      app.layout=html.Div([
      dcc.Store(id='selected'),
      dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
      dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
      dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
      dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
      dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
      ])

      In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

      In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

      app.layout=html.Div([
      dcc.Store(id='selected', selectedids=union(
      dash.properties.Input('graph-1', 'selectedids'),
      dash.properties.Input('graph-2', 'selectedids'),
      dash.properties.Input('graph-3', 'selectedids'),
      dash.properties.Input('graph-4', 'selectedids'),
      dash.properties.Input('datatable', 'selected_row_ids'),
      )),
      dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
      dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
      dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
      dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
      dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
      ])

      (Transformations might bring up some other issues - exploring these below)

    3. The community has brought this up a few times:


    Some other considerations

    This is about as far as I've thought so far. Here are some other questions & ideas to explore.

    • Data-* properties?
      With this API, Synced only works with top-level properties.
      dcc.Store only has a single top-level property for storing data: data
      So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

      It'd be nice if you could specify arbitrary properties on a single dcc.Store:

      dcc.Store(id='store'),
      dcc.Dropdown(value=Synced('store', 'data-model')),
      dcc.Input(value=Synced('store', 'data-velocity')),

      We have special support for aria-* and data-* properties in components, so
      maybe we could use that?

    • Transformations?
      Through careful prop design across components, many properties could be 1-1 with each other.
      However, there might be some synced components that would require a (inverse)transformation.
      For example, two input components: one in lbs, the other in kgs.
      We might be able to just allow two callbacks for arbitrary transformations:

      app.layout=html.Div([
      dcc.Input(id='lbs'),
      dcc.Input(id='kg'),
      ])
      @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
      returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
      returnvalue/2.2

      Of course, if we supported these callback expressions then we wouldn't
      necessarily need the special layout property dash.properties.Synced, as
      a user could just write identity callbacks:

      app.layout=html.Div([
      dcc.Store(id='selected'),
      dcc.Graph(id='graph-1'),
      dcc.Graph(id='graph-2'),
      dcc.Graph(id='graph-3'),
      dcc.Graph(id='graph-4'),
      dash_table.DataTable(id='datatable')
      ])
      defidentity(i):
      returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
      app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
      app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
      app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
      app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
      app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
      app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
      app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
      app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
      app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

      And these could be written with clientside_callbacks too.

      The union/intersect version of crossfiltering would have a different cyclical transformation:

      app.layout=html.Div([
      dcc.Store(id='selected'),
      dcc.Graph(id='graph-1'),
      dcc.Graph(id='graph-2'),
      dcc.Graph(id='graph-3'),
      dcc.Graph(id='graph-4'),
      dash_table.DataTable(id='datatable')
      ])
      defintersect(*args):
      returnreduce(np.intersect1d, args)
      defidentity(i):
      returniapp.callback(
      Output('selected', 'data'),
      [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
      [Input('datatable', 'selected_row_ids')]
      )(intersect)
      app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
      app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
      app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
      app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
      app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

      So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

      Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

      app.layout=html.Div([
      dcc.Store(id='store_model'),
      dcc.Store(id='store_velocity'),
      dcc.Tabs(
      id='tabs',
      tabs=[
      dcc.Tab(label='Controls', value='controls'),
      dcc.Tab(label='Model', value='model'),
      ],
      value='controls'
      ),
      html.Div(id='content')
      ])
      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model'
      ),
      dcc.Input(
      id='velocity'
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])
      @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
      # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
      # ...?

      In this case, our cyclical callbacks aren't handling default values.

      Perhaps we need a notion of default values & undefined values.

      Then, our initialization routine would:

      1. Analyze the cyclical graph and look for default values & non-undefined values
      2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
      3. Firing everything in one complete circle:
      app.layout=html.Div([
      dcc.Store(id='store_model'),
      dcc.Store(id='store_velocity'),
      dcc.Tabs(
      id='tabs',
      tabs=[
      dcc.Tab(label='Controls', value='controls'),
      dcc.Tab(label='Model', value='model'),
      ],
      value='controls'
      ),
      html.Div(id='content')
      ])
      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model',
      value=dash.properties.Default('mtl') # <--- provide a default value
      ),
      dcc.Input(
      id='velocity'
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])
      @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
      returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
      returnstore_value

      Lifecycle:

      1. Page is loaded, store is undefined, tab 1 is selected

      2. Dropdown is rendered. Analyze the elements in the callback graph:

        • Store is undefined
        • Dropdown has a default value

        So, since dropdown has a default, so fire the dropdown->store callback

      3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

      4. Select the second tab.

      5. Select & render the first tab. Analyze the elements in the callback graph:

        • Store is defined
        • Dropdown has a default value

        In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

      This seems to work...

      It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

      In that case (where everything is defined), which callbacks do we fire on initialization?

      • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

      • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

        app.layout=html.Div([
        dcc.Store(id='selected'),
        dcc.Graph(id='graph-1', selected=['a', 'b']),
        dcc.Graph(id='graph-2', selected=['c', 'b']),
        dcc.Graph(id='graph-3', selected=['d', 'e']),
        dcc.Graph(id='graph-4', selected=['e', 'f']),
        dash_table.DataTable(id='datatable', selected=['a', 'g'])
        ])

        in which case, the system would evolve into:

        • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
        • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
        • Circle is complete.
      • If all of the values are defined at start, then it becomes ambiguous where to start

      app.layout=html.Div([
      dcc.Store(id='selected', selected=['a', 'b']),
      dcc.Graph(id='graph-1', selected=['a', 'b']),
      dcc.Graph(id='graph-2', selected=['c', 'b']),
      dcc.Graph(id='graph-3', selected=['d', 'e']),
      dcc.Graph(id='graph-4', selected=['e', 'f']),
      dash_table.DataTable(id='datatable', selected=['a', 'g'])
      ])

      If we start with Graphs->Store, then we get:

        1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
        1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

      But if we start Store->Graphs, then we get:

        1. store->graph-1, store->graph-2, ...: ['a', 'b']
        1. graphs->store: ['a', 'b']

      However, the user provided an "inconsistent" system, so perhaps this is OK.

      We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

    Metadata

    Metadata

    Assignees

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

      Description

      @chriddyp

      A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

      Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

      However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

      Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

      At this point, I don't have any concrete recommendations, I am just sharing my explorations.


      Persisting values across tabs

      Here's a classic example: an app with two tabs.

      The first tab contains all of the controls.

      The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

      The code below does not work but it is how users would intuitively write an example like this.
      That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

      In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

      This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
      Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

      In summary, there are two challenges to this:

      1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
      2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

      This is how users are currently writing these applications (this example does not work):

      app.layout=html.Div([
      dcc.Tabs(
      id='tabs',
      tabs=[
      dcc.Tab(label='Controls', value='controls'),
      dcc.Tab(label='Model', value='model'),
      ],
      value='controls'
      ),
      html.Div(id='content')
      ])
      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model',
      # ...
      ),
      dcc.Input(
      id='velocity'# ...
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])
      @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
      # ...

      In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


      One option might be "two-way synced" components. That is, components that have the same
      value and update each other whenever either one of them changes.

      With synced components, the Dash developer could sync the controls with a global store component:

      Here's what this might look like

      app.layout=html.Div([
      dcc.Store(id='store_model'),
      dcc.Store(id='store_velocity'),
      dcc.Tabs(
      id='tabs',
      tabs=[
      dcc.Tab(label='Controls', value='controls'),
      dcc.Tab(label='Model', value='model'),
      ],
      value='controls'
      ),
      html.Div(id='content')
      ])
      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model',
      value='mtl'# ...
      ),
      dcc.Input(
      id='velocity'# ...
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])
      @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
      # ...# <-- New line - some type of sync APIapp.sync(
      Sync('store_model', 'data'),
      Sync('model', 'value')
      )
      app.sync(
      Sync('store_velocity', 'data'),
      Sync('velocity', 'value')
      )

      One challenge with this approach is the initialization logic could be ambiguous.
      Here's the lifecycle in this app:

      1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
      2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
      3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
      4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

      In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

      app.sync(
      Sync(id='store_model', property='data', default=None),
      Sync('model', 'value', default='mtl')
      )

      That's not great though, because then the default can't be conditional.

      Alternatively, the Sync property could be something provided in the layout. A new special property:

      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model',
      value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
      target_property='data',
      default='mtl'# <-- This could be dynamic
      )
      # ...
      ),
      dcc.Input(
      id='velocity'value=dash.properties.Synced(
      target_id='store_velocity',
      target_property='data',
      default=''
      )
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])

      In this case, the logic would be:

      1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
        In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
      2. If the synced component's "target"'s value is defined, then use that instead.
        In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

      Alternatively, the default value could be pushed into the dcc.Store:

      app.layout=html.Div([
      dcc.Store(id='store_model', data='mtl'),
      dcc.Store(id='store_velocity', data=0),
      # tabs & content ...
      ])
      @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
      ifselected_tab=='controls':
      returnhtml.Div([
      dcc.Dropdown(
      id='model',
      value=dash.properties.Synced(
      id='store_model',
      property='data',
      )
      ),
      dcc.Input(
      id='velocity'value=dash.properties.Synced(
      id='store_velocity',
      property='data'
      )
      )
      ])
      elifselected_tab=='model':
      returnhtml.Div([
      dcc.Graph(
      id='graph-1'
      ),
      ])

      But in this model, the property value couldn't be dynamic.


      Once potentially nice feature of using dcc.Store for this is the ability to save values
      into localstorage across page loads. Lifecycle:

      1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
      2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
      3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
      4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

      Other Synced applications

      Two-way sync has come up in a few other cases:

      1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
      app.layout=html.Div([
      dcc.Location(id='url'),
      dcc.Tabs(
      id='tabs',
      value=dash.properties.Synced(
      id='url',
      property='href'
      ),
      children=[
      dcc.Tab(value='/page-1', label='Analysis 1'),
      dcc.Tab(value='/page-2', label='Analysis 2'),
      ]
      ),
      html.Div(id='output')
      ])
      @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
      iftab=='/page-1':
      # ...eliftab=='/page-1':
      # ...
      1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
        image

      2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

        app.layout=html.Div([
        dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
        dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
        dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
        dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
        ])

        These could even be synced up with a dash_table.DataTable.selected_row_ids:

        app.layout=html.Div([
        dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
        dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
        dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
        dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
        dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
        ])

        Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

        app.layout=html.Div([
        dcc.Store(id='selected'),
        dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
        dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
        dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
        dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
        dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
        ])

        In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

        In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

        app.layout=html.Div([
        dcc.Store(id='selected', selectedids=union(
        dash.properties.Input('graph-1', 'selectedids'),
        dash.properties.Input('graph-2', 'selectedids'),
        dash.properties.Input('graph-3', 'selectedids'),
        dash.properties.Input('graph-4', 'selectedids'),
        dash.properties.Input('datatable', 'selected_row_ids'),
        )),
        dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
        dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
        dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
        dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
        dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
        ])

        (Transformations might bring up some other issues - exploring these below)

      3. The community has brought this up a few times:


      Some other considerations

      This is about as far as I've thought so far. Here are some other questions & ideas to explore.

      • Data-* properties?
        With this API, Synced only works with top-level properties.
        dcc.Store only has a single top-level property for storing data: data
        So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

        It'd be nice if you could specify arbitrary properties on a single dcc.Store:

        dcc.Store(id='store'),
        dcc.Dropdown(value=Synced('store', 'data-model')),
        dcc.Input(value=Synced('store', 'data-velocity')),

        We have special support for aria-* and data-* properties in components, so
        maybe we could use that?

      • Transformations?
        Through careful prop design across components, many properties could be 1-1 with each other.
        However, there might be some synced components that would require a (inverse)transformation.
        For example, two input components: one in lbs, the other in kgs.
        We might be able to just allow two callbacks for arbitrary transformations:

        app.layout=html.Div([
        dcc.Input(id='lbs'),
        dcc.Input(id='kg'),
        ])
        @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
        returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
        returnvalue/2.2

        Of course, if we supported these callback expressions then we wouldn't
        necessarily need the special layout property dash.properties.Synced, as
        a user could just write identity callbacks:

        app.layout=html.Div([
        dcc.Store(id='selected'),
        dcc.Graph(id='graph-1'),
        dcc.Graph(id='graph-2'),
        dcc.Graph(id='graph-3'),
        dcc.Graph(id='graph-4'),
        dash_table.DataTable(id='datatable')
        ])
        defidentity(i):
        returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
        app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
        app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
        app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
        app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
        app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
        app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
        app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
        app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
        app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

        And these could be written with clientside_callbacks too.

        The union/intersect version of crossfiltering would have a different cyclical transformation:

        app.layout=html.Div([
        dcc.Store(id='selected'),
        dcc.Graph(id='graph-1'),
        dcc.Graph(id='graph-2'),
        dcc.Graph(id='graph-3'),
        dcc.Graph(id='graph-4'),
        dash_table.DataTable(id='datatable')
        ])
        defintersect(*args):
        returnreduce(np.intersect1d, args)
        defidentity(i):
        returniapp.callback(
        Output('selected', 'data'),
        [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
        [Input('datatable', 'selected_row_ids')]
        )(intersect)
        app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
        app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
        app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
        app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
        app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

        So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

        Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

        app.layout=html.Div([
        dcc.Store(id='store_model'),
        dcc.Store(id='store_velocity'),
        dcc.Tabs(
        id='tabs',
        tabs=[
        dcc.Tab(label='Controls', value='controls'),
        dcc.Tab(label='Model', value='model'),
        ],
        value='controls'
        ),
        html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model'
        ),
        dcc.Input(
        id='velocity'
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])
        @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
        # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
        # ...?

        In this case, our cyclical callbacks aren't handling default values.

        Perhaps we need a notion of default values & undefined values.

        Then, our initialization routine would:

        1. Analyze the cyclical graph and look for default values & non-undefined values
        2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
        3. Firing everything in one complete circle:
        app.layout=html.Div([
        dcc.Store(id='store_model'),
        dcc.Store(id='store_velocity'),
        dcc.Tabs(
        id='tabs',
        tabs=[
        dcc.Tab(label='Controls', value='controls'),
        dcc.Tab(label='Model', value='model'),
        ],
        value='controls'
        ),
        html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model',
        value=dash.properties.Default('mtl') # <--- provide a default value
        ),
        dcc.Input(
        id='velocity'
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])
        @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
        returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
        returnstore_value

        Lifecycle:

        1. Page is loaded, store is undefined, tab 1 is selected

        2. Dropdown is rendered. Analyze the elements in the callback graph:

          • Store is undefined
          • Dropdown has a default value

          So, since dropdown has a default, so fire the dropdown->store callback

        3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

        4. Select the second tab.

        5. Select & render the first tab. Analyze the elements in the callback graph:

          • Store is defined
          • Dropdown has a default value

          In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

        This seems to work...

        It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

        In that case (where everything is defined), which callbacks do we fire on initialization?

        • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

        • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

          app.layout=html.Div([
          dcc.Store(id='selected'),
          dcc.Graph(id='graph-1', selected=['a', 'b']),
          dcc.Graph(id='graph-2', selected=['c', 'b']),
          dcc.Graph(id='graph-3', selected=['d', 'e']),
          dcc.Graph(id='graph-4', selected=['e', 'f']),
          dash_table.DataTable(id='datatable', selected=['a', 'g'])
          ])

          in which case, the system would evolve into:

          • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
          • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
          • Circle is complete.
        • If all of the values are defined at start, then it becomes ambiguous where to start

        app.layout=html.Div([
        dcc.Store(id='selected', selected=['a', 'b']),
        dcc.Graph(id='graph-1', selected=['a', 'b']),
        dcc.Graph(id='graph-2', selected=['c', 'b']),
        dcc.Graph(id='graph-3', selected=['d', 'e']),
        dcc.Graph(id='graph-4', selected=['e', 'f']),
        dash_table.DataTable(id='datatable', selected=['a', 'g'])
        ])

        If we start with Graphs->Store, then we get:

          1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
          1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

        But if we start Store->Graphs, then we get:

          1. store->graph-1, store->graph-2, ...: ['a', 'b']
          1. graphs->store: ['a', 'b']

        However, the user provided an "inconsistent" system, so perhaps this is OK.

        We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

      Metadata

      Metadata

      Assignees

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

        Description

        @chriddyp

        A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

        Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

        However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

        Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

        At this point, I don't have any concrete recommendations, I am just sharing my explorations.


        Persisting values across tabs

        Here's a classic example: an app with two tabs.

        The first tab contains all of the controls.

        The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

        The code below does not work but it is how users would intuitively write an example like this.
        That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

        In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

        This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
        Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

        In summary, there are two challenges to this:

        1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
        2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

        This is how users are currently writing these applications (this example does not work):

        app.layout=html.Div([
        dcc.Tabs(
        id='tabs',
        tabs=[
        dcc.Tab(label='Controls', value='controls'),
        dcc.Tab(label='Model', value='model'),
        ],
        value='controls'
        ),
        html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model',
        # ...
        ),
        dcc.Input(
        id='velocity'# ...
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])
        @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
        # ...

        In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


        One option might be "two-way synced" components. That is, components that have the same
        value and update each other whenever either one of them changes.

        With synced components, the Dash developer could sync the controls with a global store component:

        Here's what this might look like

        app.layout=html.Div([
        dcc.Store(id='store_model'),
        dcc.Store(id='store_velocity'),
        dcc.Tabs(
        id='tabs',
        tabs=[
        dcc.Tab(label='Controls', value='controls'),
        dcc.Tab(label='Model', value='model'),
        ],
        value='controls'
        ),
        html.Div(id='content')
        ])
        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model',
        value='mtl'# ...
        ),
        dcc.Input(
        id='velocity'# ...
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])
        @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
        # ...# <-- New line - some type of sync APIapp.sync(
        Sync('store_model', 'data'),
        Sync('model', 'value')
        )
        app.sync(
        Sync('store_velocity', 'data'),
        Sync('velocity', 'value')
        )

        One challenge with this approach is the initialization logic could be ambiguous.
        Here's the lifecycle in this app:

        1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
        2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
        3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
        4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

        In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

        app.sync(
        Sync(id='store_model', property='data', default=None),
        Sync('model', 'value', default='mtl')
        )

        That's not great though, because then the default can't be conditional.

        Alternatively, the Sync property could be something provided in the layout. A new special property:

        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model',
        value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
        target_property='data',
        default='mtl'# <-- This could be dynamic
        )
        # ...
        ),
        dcc.Input(
        id='velocity'value=dash.properties.Synced(
        target_id='store_velocity',
        target_property='data',
        default=''
        )
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])

        In this case, the logic would be:

        1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
          In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
        2. If the synced component's "target"'s value is defined, then use that instead.
          In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

        Alternatively, the default value could be pushed into the dcc.Store:

        app.layout=html.Div([
        dcc.Store(id='store_model', data='mtl'),
        dcc.Store(id='store_velocity', data=0),
        # tabs & content ...
        ])
        @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
        ifselected_tab=='controls':
        returnhtml.Div([
        dcc.Dropdown(
        id='model',
        value=dash.properties.Synced(
        id='store_model',
        property='data',
        )
        ),
        dcc.Input(
        id='velocity'value=dash.properties.Synced(
        id='store_velocity',
        property='data'
        )
        )
        ])
        elifselected_tab=='model':
        returnhtml.Div([
        dcc.Graph(
        id='graph-1'
        ),
        ])

        But in this model, the property value couldn't be dynamic.


        Once potentially nice feature of using dcc.Store for this is the ability to save values
        into localstorage across page loads. Lifecycle:

        1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
        2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
        3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
        4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

        Other Synced applications

        Two-way sync has come up in a few other cases:

        1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
        app.layout=html.Div([
        dcc.Location(id='url'),
        dcc.Tabs(
        id='tabs',
        value=dash.properties.Synced(
        id='url',
        property='href'
        ),
        children=[
        dcc.Tab(value='/page-1', label='Analysis 1'),
        dcc.Tab(value='/page-2', label='Analysis 2'),
        ]
        ),
        html.Div(id='output')
        ])
        @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
        iftab=='/page-1':
        # ...eliftab=='/page-1':
        # ...
        1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
          image

        2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

          app.layout=html.Div([
          dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
          dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
          dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
          dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
          ])

          These could even be synced up with a dash_table.DataTable.selected_row_ids:

          app.layout=html.Div([
          dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
          dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
          dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
          dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
          dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
          ])

          Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

          app.layout=html.Div([
          dcc.Store(id='selected'),
          dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
          dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
          dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
          dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
          dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
          ])

          In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

          In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

          app.layout=html.Div([
          dcc.Store(id='selected', selectedids=union(
          dash.properties.Input('graph-1', 'selectedids'),
          dash.properties.Input('graph-2', 'selectedids'),
          dash.properties.Input('graph-3', 'selectedids'),
          dash.properties.Input('graph-4', 'selectedids'),
          dash.properties.Input('datatable', 'selected_row_ids'),
          )),
          dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
          dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
          dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
          dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
          dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
          ])

          (Transformations might bring up some other issues - exploring these below)

        3. The community has brought this up a few times:


        Some other considerations

        This is about as far as I've thought so far. Here are some other questions & ideas to explore.

        • Data-* properties?
          With this API, Synced only works with top-level properties.
          dcc.Store only has a single top-level property for storing data: data
          So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

          It'd be nice if you could specify arbitrary properties on a single dcc.Store:

          dcc.Store(id='store'),
          dcc.Dropdown(value=Synced('store', 'data-model')),
          dcc.Input(value=Synced('store', 'data-velocity')),

          We have special support for aria-* and data-* properties in components, so
          maybe we could use that?

        • Transformations?
          Through careful prop design across components, many properties could be 1-1 with each other.
          However, there might be some synced components that would require a (inverse)transformation.
          For example, two input components: one in lbs, the other in kgs.
          We might be able to just allow two callbacks for arbitrary transformations:

          app.layout=html.Div([
          dcc.Input(id='lbs'),
          dcc.Input(id='kg'),
          ])
          @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
          returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
          returnvalue/2.2

          Of course, if we supported these callback expressions then we wouldn't
          necessarily need the special layout property dash.properties.Synced, as
          a user could just write identity callbacks:

          app.layout=html.Div([
          dcc.Store(id='selected'),
          dcc.Graph(id='graph-1'),
          dcc.Graph(id='graph-2'),
          dcc.Graph(id='graph-3'),
          dcc.Graph(id='graph-4'),
          dash_table.DataTable(id='datatable')
          ])
          defidentity(i):
          returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
          app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
          app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
          app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
          app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
          app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
          app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
          app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
          app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
          app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

          And these could be written with clientside_callbacks too.

          The union/intersect version of crossfiltering would have a different cyclical transformation:

          app.layout=html.Div([
          dcc.Store(id='selected'),
          dcc.Graph(id='graph-1'),
          dcc.Graph(id='graph-2'),
          dcc.Graph(id='graph-3'),
          dcc.Graph(id='graph-4'),
          dash_table.DataTable(id='datatable')
          ])
          defintersect(*args):
          returnreduce(np.intersect1d, args)
          defidentity(i):
          returniapp.callback(
          Output('selected', 'data'),
          [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
          [Input('datatable', 'selected_row_ids')]
          )(intersect)
          app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
          app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
          app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
          app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
          app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

          So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

          Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

          app.layout=html.Div([
          dcc.Store(id='store_model'),
          dcc.Store(id='store_velocity'),
          dcc.Tabs(
          id='tabs',
          tabs=[
          dcc.Tab(label='Controls', value='controls'),
          dcc.Tab(label='Model', value='model'),
          ],
          value='controls'
          ),
          html.Div(id='content')
          ])
          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model'
          ),
          dcc.Input(
          id='velocity'
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])
          @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
          # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
          # ...?

          In this case, our cyclical callbacks aren't handling default values.

          Perhaps we need a notion of default values & undefined values.

          Then, our initialization routine would:

          1. Analyze the cyclical graph and look for default values & non-undefined values
          2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
          3. Firing everything in one complete circle:
          app.layout=html.Div([
          dcc.Store(id='store_model'),
          dcc.Store(id='store_velocity'),
          dcc.Tabs(
          id='tabs',
          tabs=[
          dcc.Tab(label='Controls', value='controls'),
          dcc.Tab(label='Model', value='model'),
          ],
          value='controls'
          ),
          html.Div(id='content')
          ])
          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model',
          value=dash.properties.Default('mtl') # <--- provide a default value
          ),
          dcc.Input(
          id='velocity'
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])
          @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
          returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
          returnstore_value

          Lifecycle:

          1. Page is loaded, store is undefined, tab 1 is selected

          2. Dropdown is rendered. Analyze the elements in the callback graph:

            • Store is undefined
            • Dropdown has a default value

            So, since dropdown has a default, so fire the dropdown->store callback

          3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

          4. Select the second tab.

          5. Select & render the first tab. Analyze the elements in the callback graph:

            • Store is defined
            • Dropdown has a default value

            In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

          This seems to work...

          It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

          In that case (where everything is defined), which callbacks do we fire on initialization?

          • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

          • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

            app.layout=html.Div([
            dcc.Store(id='selected'),
            dcc.Graph(id='graph-1', selected=['a', 'b']),
            dcc.Graph(id='graph-2', selected=['c', 'b']),
            dcc.Graph(id='graph-3', selected=['d', 'e']),
            dcc.Graph(id='graph-4', selected=['e', 'f']),
            dash_table.DataTable(id='datatable', selected=['a', 'g'])
            ])

            in which case, the system would evolve into:

            • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
            • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
            • Circle is complete.
          • If all of the values are defined at start, then it becomes ambiguous where to start

          app.layout=html.Div([
          dcc.Store(id='selected', selected=['a', 'b']),
          dcc.Graph(id='graph-1', selected=['a', 'b']),
          dcc.Graph(id='graph-2', selected=['c', 'b']),
          dcc.Graph(id='graph-3', selected=['d', 'e']),
          dcc.Graph(id='graph-4', selected=['e', 'f']),
          dash_table.DataTable(id='datatable', selected=['a', 'g'])
          ])

          If we start with Graphs->Store, then we get:

            1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
            1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

          But if we start Store->Graphs, then we get:

            1. store->graph-1, store->graph-2, ...: ['a', 'b']
            1. graphs->store: ['a', 'b']

          However, the user provided an "inconsistent" system, so perhaps this is OK.

          We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

        Metadata

        Metadata

        Assignees

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

          Description

          @chriddyp

          A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

          Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

          However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

          Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

          At this point, I don't have any concrete recommendations, I am just sharing my explorations.


          Persisting values across tabs

          Here's a classic example: an app with two tabs.

          The first tab contains all of the controls.

          The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

          The code below does not work but it is how users would intuitively write an example like this.
          That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

          In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

          This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
          Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

          In summary, there are two challenges to this:

          1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
          2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

          This is how users are currently writing these applications (this example does not work):

          app.layout=html.Div([
          dcc.Tabs(
          id='tabs',
          tabs=[
          dcc.Tab(label='Controls', value='controls'),
          dcc.Tab(label='Model', value='model'),
          ],
          value='controls'
          ),
          html.Div(id='content')
          ])
          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model',
          # ...
          ),
          dcc.Input(
          id='velocity'# ...
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])
          @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
          # ...

          In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


          One option might be "two-way synced" components. That is, components that have the same
          value and update each other whenever either one of them changes.

          With synced components, the Dash developer could sync the controls with a global store component:

          Here's what this might look like

          app.layout=html.Div([
          dcc.Store(id='store_model'),
          dcc.Store(id='store_velocity'),
          dcc.Tabs(
          id='tabs',
          tabs=[
          dcc.Tab(label='Controls', value='controls'),
          dcc.Tab(label='Model', value='model'),
          ],
          value='controls'
          ),
          html.Div(id='content')
          ])
          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model',
          value='mtl'# ...
          ),
          dcc.Input(
          id='velocity'# ...
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])
          @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
          # ...# <-- New line - some type of sync APIapp.sync(
          Sync('store_model', 'data'),
          Sync('model', 'value')
          )
          app.sync(
          Sync('store_velocity', 'data'),
          Sync('velocity', 'value')
          )

          One challenge with this approach is the initialization logic could be ambiguous.
          Here's the lifecycle in this app:

          1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
          2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
          3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
          4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

          In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

          app.sync(
          Sync(id='store_model', property='data', default=None),
          Sync('model', 'value', default='mtl')
          )

          That's not great though, because then the default can't be conditional.

          Alternatively, the Sync property could be something provided in the layout. A new special property:

          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model',
          value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
          target_property='data',
          default='mtl'# <-- This could be dynamic
          )
          # ...
          ),
          dcc.Input(
          id='velocity'value=dash.properties.Synced(
          target_id='store_velocity',
          target_property='data',
          default=''
          )
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])

          In this case, the logic would be:

          1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
            In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
          2. If the synced component's "target"'s value is defined, then use that instead.
            In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

          Alternatively, the default value could be pushed into the dcc.Store:

          app.layout=html.Div([
          dcc.Store(id='store_model', data='mtl'),
          dcc.Store(id='store_velocity', data=0),
          # tabs & content ...
          ])
          @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
          ifselected_tab=='controls':
          returnhtml.Div([
          dcc.Dropdown(
          id='model',
          value=dash.properties.Synced(
          id='store_model',
          property='data',
          )
          ),
          dcc.Input(
          id='velocity'value=dash.properties.Synced(
          id='store_velocity',
          property='data'
          )
          )
          ])
          elifselected_tab=='model':
          returnhtml.Div([
          dcc.Graph(
          id='graph-1'
          ),
          ])

          But in this model, the property value couldn't be dynamic.


          Once potentially nice feature of using dcc.Store for this is the ability to save values
          into localstorage across page loads. Lifecycle:

          1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
          2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
          3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
          4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

          Other Synced applications

          Two-way sync has come up in a few other cases:

          1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
          app.layout=html.Div([
          dcc.Location(id='url'),
          dcc.Tabs(
          id='tabs',
          value=dash.properties.Synced(
          id='url',
          property='href'
          ),
          children=[
          dcc.Tab(value='/page-1', label='Analysis 1'),
          dcc.Tab(value='/page-2', label='Analysis 2'),
          ]
          ),
          html.Div(id='output')
          ])
          @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
          iftab=='/page-1':
          # ...eliftab=='/page-1':
          # ...
          1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
            image

          2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

            app.layout=html.Div([
            dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
            dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
            dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
            dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
            ])

            These could even be synced up with a dash_table.DataTable.selected_row_ids:

            app.layout=html.Div([
            dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
            dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
            dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
            dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
            dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
            ])

            Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

            app.layout=html.Div([
            dcc.Store(id='selected'),
            dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
            dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
            dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
            dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
            dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
            ])

            In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

            In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

            app.layout=html.Div([
            dcc.Store(id='selected', selectedids=union(
            dash.properties.Input('graph-1', 'selectedids'),
            dash.properties.Input('graph-2', 'selectedids'),
            dash.properties.Input('graph-3', 'selectedids'),
            dash.properties.Input('graph-4', 'selectedids'),
            dash.properties.Input('datatable', 'selected_row_ids'),
            )),
            dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
            dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
            dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
            dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
            dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
            ])

            (Transformations might bring up some other issues - exploring these below)

          3. The community has brought this up a few times:


          Some other considerations

          This is about as far as I've thought so far. Here are some other questions & ideas to explore.

          • Data-* properties?
            With this API, Synced only works with top-level properties.
            dcc.Store only has a single top-level property for storing data: data
            So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

            It'd be nice if you could specify arbitrary properties on a single dcc.Store:

            dcc.Store(id='store'),
            dcc.Dropdown(value=Synced('store', 'data-model')),
            dcc.Input(value=Synced('store', 'data-velocity')),

            We have special support for aria-* and data-* properties in components, so
            maybe we could use that?

          • Transformations?
            Through careful prop design across components, many properties could be 1-1 with each other.
            However, there might be some synced components that would require a (inverse)transformation.
            For example, two input components: one in lbs, the other in kgs.
            We might be able to just allow two callbacks for arbitrary transformations:

            app.layout=html.Div([
            dcc.Input(id='lbs'),
            dcc.Input(id='kg'),
            ])
            @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
            returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
            returnvalue/2.2

            Of course, if we supported these callback expressions then we wouldn't
            necessarily need the special layout property dash.properties.Synced, as
            a user could just write identity callbacks:

            app.layout=html.Div([
            dcc.Store(id='selected'),
            dcc.Graph(id='graph-1'),
            dcc.Graph(id='graph-2'),
            dcc.Graph(id='graph-3'),
            dcc.Graph(id='graph-4'),
            dash_table.DataTable(id='datatable')
            ])
            defidentity(i):
            returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
            app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
            app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
            app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
            app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
            app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
            app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
            app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
            app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
            app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

            And these could be written with clientside_callbacks too.

            The union/intersect version of crossfiltering would have a different cyclical transformation:

            app.layout=html.Div([
            dcc.Store(id='selected'),
            dcc.Graph(id='graph-1'),
            dcc.Graph(id='graph-2'),
            dcc.Graph(id='graph-3'),
            dcc.Graph(id='graph-4'),
            dash_table.DataTable(id='datatable')
            ])
            defintersect(*args):
            returnreduce(np.intersect1d, args)
            defidentity(i):
            returniapp.callback(
            Output('selected', 'data'),
            [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
            [Input('datatable', 'selected_row_ids')]
            )(intersect)
            app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
            app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
            app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
            app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
            app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

            So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

            Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

            app.layout=html.Div([
            dcc.Store(id='store_model'),
            dcc.Store(id='store_velocity'),
            dcc.Tabs(
            id='tabs',
            tabs=[
            dcc.Tab(label='Controls', value='controls'),
            dcc.Tab(label='Model', value='model'),
            ],
            value='controls'
            ),
            html.Div(id='content')
            ])
            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model'
            ),
            dcc.Input(
            id='velocity'
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])
            @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
            # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
            # ...?

            In this case, our cyclical callbacks aren't handling default values.

            Perhaps we need a notion of default values & undefined values.

            Then, our initialization routine would:

            1. Analyze the cyclical graph and look for default values & non-undefined values
            2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
            3. Firing everything in one complete circle:
            app.layout=html.Div([
            dcc.Store(id='store_model'),
            dcc.Store(id='store_velocity'),
            dcc.Tabs(
            id='tabs',
            tabs=[
            dcc.Tab(label='Controls', value='controls'),
            dcc.Tab(label='Model', value='model'),
            ],
            value='controls'
            ),
            html.Div(id='content')
            ])
            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model',
            value=dash.properties.Default('mtl') # <--- provide a default value
            ),
            dcc.Input(
            id='velocity'
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])
            @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
            returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
            returnstore_value

            Lifecycle:

            1. Page is loaded, store is undefined, tab 1 is selected

            2. Dropdown is rendered. Analyze the elements in the callback graph:

              • Store is undefined
              • Dropdown has a default value

              So, since dropdown has a default, so fire the dropdown->store callback

            3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

            4. Select the second tab.

            5. Select & render the first tab. Analyze the elements in the callback graph:

              • Store is defined
              • Dropdown has a default value

              In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

            This seems to work...

            It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

            In that case (where everything is defined), which callbacks do we fire on initialization?

            • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

            • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

              app.layout=html.Div([
              dcc.Store(id='selected'),
              dcc.Graph(id='graph-1', selected=['a', 'b']),
              dcc.Graph(id='graph-2', selected=['c', 'b']),
              dcc.Graph(id='graph-3', selected=['d', 'e']),
              dcc.Graph(id='graph-4', selected=['e', 'f']),
              dash_table.DataTable(id='datatable', selected=['a', 'g'])
              ])

              in which case, the system would evolve into:

              • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
              • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
              • Circle is complete.
            • If all of the values are defined at start, then it becomes ambiguous where to start

            app.layout=html.Div([
            dcc.Store(id='selected', selected=['a', 'b']),
            dcc.Graph(id='graph-1', selected=['a', 'b']),
            dcc.Graph(id='graph-2', selected=['c', 'b']),
            dcc.Graph(id='graph-3', selected=['d', 'e']),
            dcc.Graph(id='graph-4', selected=['e', 'f']),
            dash_table.DataTable(id='datatable', selected=['a', 'g'])
            ])

            If we start with Graphs->Store, then we get:

              1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
              1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

            But if we start Store->Graphs, then we get:

              1. store->graph-1, store->graph-2, ...: ['a', 'b']
              1. graphs->store: ['a', 'b']

            However, the user provided an "inconsistent" system, so perhaps this is OK.

            We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

          Metadata

          Metadata

          Assignees

          Labels

          No labels
          No labels

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

            Description

            @chriddyp

            A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

            Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

            However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

            Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

            At this point, I don't have any concrete recommendations, I am just sharing my explorations.


            Persisting values across tabs

            Here's a classic example: an app with two tabs.

            The first tab contains all of the controls.

            The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

            The code below does not work but it is how users would intuitively write an example like this.
            That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

            In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

            This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
            Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

            In summary, there are two challenges to this:

            1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
            2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

            This is how users are currently writing these applications (this example does not work):

            app.layout=html.Div([
            dcc.Tabs(
            id='tabs',
            tabs=[
            dcc.Tab(label='Controls', value='controls'),
            dcc.Tab(label='Model', value='model'),
            ],
            value='controls'
            ),
            html.Div(id='content')
            ])
            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model',
            # ...
            ),
            dcc.Input(
            id='velocity'# ...
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])
            @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
            # ...

            In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


            One option might be "two-way synced" components. That is, components that have the same
            value and update each other whenever either one of them changes.

            With synced components, the Dash developer could sync the controls with a global store component:

            Here's what this might look like

            app.layout=html.Div([
            dcc.Store(id='store_model'),
            dcc.Store(id='store_velocity'),
            dcc.Tabs(
            id='tabs',
            tabs=[
            dcc.Tab(label='Controls', value='controls'),
            dcc.Tab(label='Model', value='model'),
            ],
            value='controls'
            ),
            html.Div(id='content')
            ])
            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model',
            value='mtl'# ...
            ),
            dcc.Input(
            id='velocity'# ...
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])
            @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
            # ...# <-- New line - some type of sync APIapp.sync(
            Sync('store_model', 'data'),
            Sync('model', 'value')
            )
            app.sync(
            Sync('store_velocity', 'data'),
            Sync('velocity', 'value')
            )

            One challenge with this approach is the initialization logic could be ambiguous.
            Here's the lifecycle in this app:

            1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
            2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
            3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
            4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

            In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

            app.sync(
            Sync(id='store_model', property='data', default=None),
            Sync('model', 'value', default='mtl')
            )

            That's not great though, because then the default can't be conditional.

            Alternatively, the Sync property could be something provided in the layout. A new special property:

            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model',
            value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
            target_property='data',
            default='mtl'# <-- This could be dynamic
            )
            # ...
            ),
            dcc.Input(
            id='velocity'value=dash.properties.Synced(
            target_id='store_velocity',
            target_property='data',
            default=''
            )
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])

            In this case, the logic would be:

            1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
              In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
            2. If the synced component's "target"'s value is defined, then use that instead.
              In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

            Alternatively, the default value could be pushed into the dcc.Store:

            app.layout=html.Div([
            dcc.Store(id='store_model', data='mtl'),
            dcc.Store(id='store_velocity', data=0),
            # tabs & content ...
            ])
            @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
            ifselected_tab=='controls':
            returnhtml.Div([
            dcc.Dropdown(
            id='model',
            value=dash.properties.Synced(
            id='store_model',
            property='data',
            )
            ),
            dcc.Input(
            id='velocity'value=dash.properties.Synced(
            id='store_velocity',
            property='data'
            )
            )
            ])
            elifselected_tab=='model':
            returnhtml.Div([
            dcc.Graph(
            id='graph-1'
            ),
            ])

            But in this model, the property value couldn't be dynamic.


            Once potentially nice feature of using dcc.Store for this is the ability to save values
            into localstorage across page loads. Lifecycle:

            1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
            2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
            3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
            4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

            Other Synced applications

            Two-way sync has come up in a few other cases:

            1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
            app.layout=html.Div([
            dcc.Location(id='url'),
            dcc.Tabs(
            id='tabs',
            value=dash.properties.Synced(
            id='url',
            property='href'
            ),
            children=[
            dcc.Tab(value='/page-1', label='Analysis 1'),
            dcc.Tab(value='/page-2', label='Analysis 2'),
            ]
            ),
            html.Div(id='output')
            ])
            @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
            iftab=='/page-1':
            # ...eliftab=='/page-1':
            # ...
            1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
              image

            2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

              app.layout=html.Div([
              dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
              dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
              dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
              dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
              ])

              These could even be synced up with a dash_table.DataTable.selected_row_ids:

              app.layout=html.Div([
              dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
              dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
              dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
              dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
              dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
              ])

              Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

              app.layout=html.Div([
              dcc.Store(id='selected'),
              dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
              dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
              dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
              dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
              dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
              ])

              In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

              In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

              app.layout=html.Div([
              dcc.Store(id='selected', selectedids=union(
              dash.properties.Input('graph-1', 'selectedids'),
              dash.properties.Input('graph-2', 'selectedids'),
              dash.properties.Input('graph-3', 'selectedids'),
              dash.properties.Input('graph-4', 'selectedids'),
              dash.properties.Input('datatable', 'selected_row_ids'),
              )),
              dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
              dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
              dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
              dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
              dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
              ])

              (Transformations might bring up some other issues - exploring these below)

            3. The community has brought this up a few times:


            Some other considerations

            This is about as far as I've thought so far. Here are some other questions & ideas to explore.

            • Data-* properties?
              With this API, Synced only works with top-level properties.
              dcc.Store only has a single top-level property for storing data: data
              So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

              It'd be nice if you could specify arbitrary properties on a single dcc.Store:

              dcc.Store(id='store'),
              dcc.Dropdown(value=Synced('store', 'data-model')),
              dcc.Input(value=Synced('store', 'data-velocity')),

              We have special support for aria-* and data-* properties in components, so
              maybe we could use that?

            • Transformations?
              Through careful prop design across components, many properties could be 1-1 with each other.
              However, there might be some synced components that would require a (inverse)transformation.
              For example, two input components: one in lbs, the other in kgs.
              We might be able to just allow two callbacks for arbitrary transformations:

              app.layout=html.Div([
              dcc.Input(id='lbs'),
              dcc.Input(id='kg'),
              ])
              @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
              returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
              returnvalue/2.2

              Of course, if we supported these callback expressions then we wouldn't
              necessarily need the special layout property dash.properties.Synced, as
              a user could just write identity callbacks:

              app.layout=html.Div([
              dcc.Store(id='selected'),
              dcc.Graph(id='graph-1'),
              dcc.Graph(id='graph-2'),
              dcc.Graph(id='graph-3'),
              dcc.Graph(id='graph-4'),
              dash_table.DataTable(id='datatable')
              ])
              defidentity(i):
              returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
              app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
              app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
              app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
              app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
              app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
              app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
              app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
              app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
              app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

              And these could be written with clientside_callbacks too.

              The union/intersect version of crossfiltering would have a different cyclical transformation:

              app.layout=html.Div([
              dcc.Store(id='selected'),
              dcc.Graph(id='graph-1'),
              dcc.Graph(id='graph-2'),
              dcc.Graph(id='graph-3'),
              dcc.Graph(id='graph-4'),
              dash_table.DataTable(id='datatable')
              ])
              defintersect(*args):
              returnreduce(np.intersect1d, args)
              defidentity(i):
              returniapp.callback(
              Output('selected', 'data'),
              [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
              [Input('datatable', 'selected_row_ids')]
              )(intersect)
              app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
              app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
              app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
              app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
              app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

              So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

              Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

              app.layout=html.Div([
              dcc.Store(id='store_model'),
              dcc.Store(id='store_velocity'),
              dcc.Tabs(
              id='tabs',
              tabs=[
              dcc.Tab(label='Controls', value='controls'),
              dcc.Tab(label='Model', value='model'),
              ],
              value='controls'
              ),
              html.Div(id='content')
              ])
              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model'
              ),
              dcc.Input(
              id='velocity'
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])
              @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
              # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
              # ...?

              In this case, our cyclical callbacks aren't handling default values.

              Perhaps we need a notion of default values & undefined values.

              Then, our initialization routine would:

              1. Analyze the cyclical graph and look for default values & non-undefined values
              2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
              3. Firing everything in one complete circle:
              app.layout=html.Div([
              dcc.Store(id='store_model'),
              dcc.Store(id='store_velocity'),
              dcc.Tabs(
              id='tabs',
              tabs=[
              dcc.Tab(label='Controls', value='controls'),
              dcc.Tab(label='Model', value='model'),
              ],
              value='controls'
              ),
              html.Div(id='content')
              ])
              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model',
              value=dash.properties.Default('mtl') # <--- provide a default value
              ),
              dcc.Input(
              id='velocity'
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])
              @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
              returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
              returnstore_value

              Lifecycle:

              1. Page is loaded, store is undefined, tab 1 is selected

              2. Dropdown is rendered. Analyze the elements in the callback graph:

                • Store is undefined
                • Dropdown has a default value

                So, since dropdown has a default, so fire the dropdown->store callback

              3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

              4. Select the second tab.

              5. Select & render the first tab. Analyze the elements in the callback graph:

                • Store is defined
                • Dropdown has a default value

                In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

              This seems to work...

              It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

              In that case (where everything is defined), which callbacks do we fire on initialization?

              • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

              • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

                app.layout=html.Div([
                dcc.Store(id='selected'),
                dcc.Graph(id='graph-1', selected=['a', 'b']),
                dcc.Graph(id='graph-2', selected=['c', 'b']),
                dcc.Graph(id='graph-3', selected=['d', 'e']),
                dcc.Graph(id='graph-4', selected=['e', 'f']),
                dash_table.DataTable(id='datatable', selected=['a', 'g'])
                ])

                in which case, the system would evolve into:

                • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
                • Circle is complete.
              • If all of the values are defined at start, then it becomes ambiguous where to start

              app.layout=html.Div([
              dcc.Store(id='selected', selected=['a', 'b']),
              dcc.Graph(id='graph-1', selected=['a', 'b']),
              dcc.Graph(id='graph-2', selected=['c', 'b']),
              dcc.Graph(id='graph-3', selected=['d', 'e']),
              dcc.Graph(id='graph-4', selected=['e', 'f']),
              dash_table.DataTable(id='datatable', selected=['a', 'g'])
              ])

              If we start with Graphs->Store, then we get:

                1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

              But if we start Store->Graphs, then we get:

                1. store->graph-1, store->graph-2, ...: ['a', 'b']
                1. graphs->store: ['a', 'b']

              However, the user provided an "inconsistent" system, so perhaps this is OK.

              We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

            Metadata

            Metadata

            Assignees

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

              Description

              @chriddyp

              A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

              Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

              However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

              Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

              At this point, I don't have any concrete recommendations, I am just sharing my explorations.


              Persisting values across tabs

              Here's a classic example: an app with two tabs.

              The first tab contains all of the controls.

              The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

              The code below does not work but it is how users would intuitively write an example like this.
              That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

              In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

              This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
              Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

              In summary, there are two challenges to this:

              1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
              2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

              This is how users are currently writing these applications (this example does not work):

              app.layout=html.Div([
              dcc.Tabs(
              id='tabs',
              tabs=[
              dcc.Tab(label='Controls', value='controls'),
              dcc.Tab(label='Model', value='model'),
              ],
              value='controls'
              ),
              html.Div(id='content')
              ])
              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model',
              # ...
              ),
              dcc.Input(
              id='velocity'# ...
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])
              @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
              # ...

              In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


              One option might be "two-way synced" components. That is, components that have the same
              value and update each other whenever either one of them changes.

              With synced components, the Dash developer could sync the controls with a global store component:

              Here's what this might look like

              app.layout=html.Div([
              dcc.Store(id='store_model'),
              dcc.Store(id='store_velocity'),
              dcc.Tabs(
              id='tabs',
              tabs=[
              dcc.Tab(label='Controls', value='controls'),
              dcc.Tab(label='Model', value='model'),
              ],
              value='controls'
              ),
              html.Div(id='content')
              ])
              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model',
              value='mtl'# ...
              ),
              dcc.Input(
              id='velocity'# ...
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])
              @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
              # ...# <-- New line - some type of sync APIapp.sync(
              Sync('store_model', 'data'),
              Sync('model', 'value')
              )
              app.sync(
              Sync('store_velocity', 'data'),
              Sync('velocity', 'value')
              )

              One challenge with this approach is the initialization logic could be ambiguous.
              Here's the lifecycle in this app:

              1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
              2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
              3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
              4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

              In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

              app.sync(
              Sync(id='store_model', property='data', default=None),
              Sync('model', 'value', default='mtl')
              )

              That's not great though, because then the default can't be conditional.

              Alternatively, the Sync property could be something provided in the layout. A new special property:

              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model',
              value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
              target_property='data',
              default='mtl'# <-- This could be dynamic
              )
              # ...
              ),
              dcc.Input(
              id='velocity'value=dash.properties.Synced(
              target_id='store_velocity',
              target_property='data',
              default=''
              )
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])

              In this case, the logic would be:

              1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
                In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
              2. If the synced component's "target"'s value is defined, then use that instead.
                In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

              Alternatively, the default value could be pushed into the dcc.Store:

              app.layout=html.Div([
              dcc.Store(id='store_model', data='mtl'),
              dcc.Store(id='store_velocity', data=0),
              # tabs & content ...
              ])
              @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
              ifselected_tab=='controls':
              returnhtml.Div([
              dcc.Dropdown(
              id='model',
              value=dash.properties.Synced(
              id='store_model',
              property='data',
              )
              ),
              dcc.Input(
              id='velocity'value=dash.properties.Synced(
              id='store_velocity',
              property='data'
              )
              )
              ])
              elifselected_tab=='model':
              returnhtml.Div([
              dcc.Graph(
              id='graph-1'
              ),
              ])

              But in this model, the property value couldn't be dynamic.


              Once potentially nice feature of using dcc.Store for this is the ability to save values
              into localstorage across page loads. Lifecycle:

              1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
              2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
              3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
              4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

              Other Synced applications

              Two-way sync has come up in a few other cases:

              1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
              app.layout=html.Div([
              dcc.Location(id='url'),
              dcc.Tabs(
              id='tabs',
              value=dash.properties.Synced(
              id='url',
              property='href'
              ),
              children=[
              dcc.Tab(value='/page-1', label='Analysis 1'),
              dcc.Tab(value='/page-2', label='Analysis 2'),
              ]
              ),
              html.Div(id='output')
              ])
              @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
              iftab=='/page-1':
              # ...eliftab=='/page-1':
              # ...
              1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
                image

              2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

                app.layout=html.Div([
                dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
                dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
                dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
                dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
                ])

                These could even be synced up with a dash_table.DataTable.selected_row_ids:

                app.layout=html.Div([
                dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
                dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
                dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
                dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
                dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
                ])

                Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

                app.layout=html.Div([
                dcc.Store(id='selected'),
                dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
                dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
                dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
                dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
                dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
                ])

                In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

                In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

                app.layout=html.Div([
                dcc.Store(id='selected', selectedids=union(
                dash.properties.Input('graph-1', 'selectedids'),
                dash.properties.Input('graph-2', 'selectedids'),
                dash.properties.Input('graph-3', 'selectedids'),
                dash.properties.Input('graph-4', 'selectedids'),
                dash.properties.Input('datatable', 'selected_row_ids'),
                )),
                dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
                dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
                dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
                dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
                dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
                ])

                (Transformations might bring up some other issues - exploring these below)

              3. The community has brought this up a few times:


              Some other considerations

              This is about as far as I've thought so far. Here are some other questions & ideas to explore.

              • Data-* properties?
                With this API, Synced only works with top-level properties.
                dcc.Store only has a single top-level property for storing data: data
                So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

                It'd be nice if you could specify arbitrary properties on a single dcc.Store:

                dcc.Store(id='store'),
                dcc.Dropdown(value=Synced('store', 'data-model')),
                dcc.Input(value=Synced('store', 'data-velocity')),

                We have special support for aria-* and data-* properties in components, so
                maybe we could use that?

              • Transformations?
                Through careful prop design across components, many properties could be 1-1 with each other.
                However, there might be some synced components that would require a (inverse)transformation.
                For example, two input components: one in lbs, the other in kgs.
                We might be able to just allow two callbacks for arbitrary transformations:

                app.layout=html.Div([
                dcc.Input(id='lbs'),
                dcc.Input(id='kg'),
                ])
                @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
                returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
                returnvalue/2.2

                Of course, if we supported these callback expressions then we wouldn't
                necessarily need the special layout property dash.properties.Synced, as
                a user could just write identity callbacks:

                app.layout=html.Div([
                dcc.Store(id='selected'),
                dcc.Graph(id='graph-1'),
                dcc.Graph(id='graph-2'),
                dcc.Graph(id='graph-3'),
                dcc.Graph(id='graph-4'),
                dash_table.DataTable(id='datatable')
                ])
                defidentity(i):
                returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
                app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
                app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
                app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
                app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
                app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
                app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
                app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
                app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
                app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

                And these could be written with clientside_callbacks too.

                The union/intersect version of crossfiltering would have a different cyclical transformation:

                app.layout=html.Div([
                dcc.Store(id='selected'),
                dcc.Graph(id='graph-1'),
                dcc.Graph(id='graph-2'),
                dcc.Graph(id='graph-3'),
                dcc.Graph(id='graph-4'),
                dash_table.DataTable(id='datatable')
                ])
                defintersect(*args):
                returnreduce(np.intersect1d, args)
                defidentity(i):
                returniapp.callback(
                Output('selected', 'data'),
                [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
                [Input('datatable', 'selected_row_ids')]
                )(intersect)
                app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
                app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
                app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
                app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
                app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

                So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

                Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

                app.layout=html.Div([
                dcc.Store(id='store_model'),
                dcc.Store(id='store_velocity'),
                dcc.Tabs(
                id='tabs',
                tabs=[
                dcc.Tab(label='Controls', value='controls'),
                dcc.Tab(label='Model', value='model'),
                ],
                value='controls'
                ),
                html.Div(id='content')
                ])
                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model'
                ),
                dcc.Input(
                id='velocity'
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])
                @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
                # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
                # ...?

                In this case, our cyclical callbacks aren't handling default values.

                Perhaps we need a notion of default values & undefined values.

                Then, our initialization routine would:

                1. Analyze the cyclical graph and look for default values & non-undefined values
                2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
                3. Firing everything in one complete circle:
                app.layout=html.Div([
                dcc.Store(id='store_model'),
                dcc.Store(id='store_velocity'),
                dcc.Tabs(
                id='tabs',
                tabs=[
                dcc.Tab(label='Controls', value='controls'),
                dcc.Tab(label='Model', value='model'),
                ],
                value='controls'
                ),
                html.Div(id='content')
                ])
                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model',
                value=dash.properties.Default('mtl') # <--- provide a default value
                ),
                dcc.Input(
                id='velocity'
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])
                @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
                returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
                returnstore_value

                Lifecycle:

                1. Page is loaded, store is undefined, tab 1 is selected

                2. Dropdown is rendered. Analyze the elements in the callback graph:

                  • Store is undefined
                  • Dropdown has a default value

                  So, since dropdown has a default, so fire the dropdown->store callback

                3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

                4. Select the second tab.

                5. Select & render the first tab. Analyze the elements in the callback graph:

                  • Store is defined
                  • Dropdown has a default value

                  In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

                This seems to work...

                It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

                In that case (where everything is defined), which callbacks do we fire on initialization?

                • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

                • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

                  app.layout=html.Div([
                  dcc.Store(id='selected'),
                  dcc.Graph(id='graph-1', selected=['a', 'b']),
                  dcc.Graph(id='graph-2', selected=['c', 'b']),
                  dcc.Graph(id='graph-3', selected=['d', 'e']),
                  dcc.Graph(id='graph-4', selected=['e', 'f']),
                  dash_table.DataTable(id='datatable', selected=['a', 'g'])
                  ])

                  in which case, the system would evolve into:

                  • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                  • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
                  • Circle is complete.
                • If all of the values are defined at start, then it becomes ambiguous where to start

                app.layout=html.Div([
                dcc.Store(id='selected', selected=['a', 'b']),
                dcc.Graph(id='graph-1', selected=['a', 'b']),
                dcc.Graph(id='graph-2', selected=['c', 'b']),
                dcc.Graph(id='graph-3', selected=['d', 'e']),
                dcc.Graph(id='graph-4', selected=['e', 'f']),
                dash_table.DataTable(id='datatable', selected=['a', 'g'])
                ])

                If we start with Graphs->Store, then we get:

                  1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                  1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

                But if we start Store->Graphs, then we get:

                  1. store->graph-1, store->graph-2, ...: ['a', 'b']
                  1. graphs->store: ['a', 'b']

                However, the user provided an "inconsistent" system, so perhaps this is OK.

                We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

              Metadata

              Metadata

              Assignees

              Labels

              No labels
              No labels

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844

                Description

                @chriddyp

                A classic problem in Dash is how to persist values selected in one tab to graphs in another tab.

                Currently, one workaround is to keep the tabs in the clientside layout, by rendering tabs via Method 2 here: https://dash.plot.ly/dash-core-components/tabs

                However, there isn't a clientside solution for persisting values across tabs using method 1 - rendering the tab's contents via callbacks.

                Here's a really long winded exploration of how this might work. Trying to solve this ended up surfacing a lot of other ideas & patterns worth considering.

                At this point, I don't have any concrete recommendations, I am just sharing my explorations.


                Persisting values across tabs

                Here's a classic example: an app with two tabs.

                The first tab contains all of the controls.

                The second tab contains the output components like graphs which are computed based off of the controls from the first tab.

                The code below does not work but it is how users would intuitively write an example like this.
                That fact that this doesn't work is pretty subtle - the callbacks won't be fired / will crash because the inputs are no longer present on the page.

                In addition, depending on the application the user may expect that the values that they have selected in the first tab remain selected when the tab is rerendered.

                This might not be the case all of the time - if these were separate pages, perhaps the Dash developer would like the app to render with its initial state.
                Or, perhaps they might like to give the user an option to 'refresh' the UI to the original/default state.

                In summary, there are two challenges to this:

                1. Referencing the values of properties of components that are no longer rendered when firing callbacks.
                2. When re-rendering the controls, allowing the Dash developer to either initialize their values with the previously selected values or initialize the values with the defaults.

                This is how users are currently writing these applications (this example does not work):

                app.layout=html.Div([
                dcc.Tabs(
                id='tabs',
                tabs=[
                dcc.Tab(label='Controls', value='controls'),
                dcc.Tab(label='Model', value='model'),
                ],
                value='controls'
                ),
                html.Div(id='content')
                ])
                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model',
                # ...
                ),
                dcc.Input(
                id='velocity'# ...
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])
                @app.callback(Output('graph-1', 'figure'), [Input('model', 'value'), # <-- this example doesn't work because this input is no longer visible on the page when the figure is getting renderedInput('velocity', 'value')])defupdate_graph_1(model, velocity):
                # ...

                In this issue, I'd like to discuss different user-facing APIs to enable this type of behaviour.


                One option might be "two-way synced" components. That is, components that have the same
                value and update each other whenever either one of them changes.

                With synced components, the Dash developer could sync the controls with a global store component:

                Here's what this might look like

                app.layout=html.Div([
                dcc.Store(id='store_model'),
                dcc.Store(id='store_velocity'),
                dcc.Tabs(
                id='tabs',
                tabs=[
                dcc.Tab(label='Controls', value='controls'),
                dcc.Tab(label='Model', value='model'),
                ],
                value='controls'
                ),
                html.Div(id='content')
                ])
                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model',
                value='mtl'# ...
                ),
                dcc.Input(
                id='velocity'# ...
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])
                @app.callback(Output('graph-1', 'figure'), [Input('store_model', 'value'), # <- output callback depends on the store nowInput('store_velocity', 'value')])defupdate_graph_1(model, velocity):
                # ...# <-- New line - some type of sync APIapp.sync(
                Sync('store_model', 'data'),
                Sync('model', 'value')
                )
                app.sync(
                Sync('store_velocity', 'data'),
                Sync('velocity', 'value')
                )

                One challenge with this approach is the initialization logic could be ambiguous.
                Here's the lifecycle in this app:

                1. dcc.Store(id='store_model') is initialized and dcc.Dropdown(id='model', value='mtl') isn't rendered
                2. First tab is selected and the dropdown is rendered. It has an initial value (value='mtl') and so one could argue that dcc.Store should get updated with that value. This is similar to how callbacks get fired with the component's property values when the component is rendered: rendering is treated the same as a user triggered action.
                3. Second tab is selected. Dropdown is hidden but the store still exists. The graph callback is fired when it gets rendered with the values from the store.
                4. First tab is selected. What happens? In (2), we argued that the store should get updated when the dropdown is updated. But now it seems ambiguous - we'd almost want the store's value to populate the dropdown's value. But then we're overwriting the Dropdown's value explicitly provided in the component returned in the callback.

                In order for this not to be ambiguous, we might need to introduce the notion of "default" properties. Perhaps this could be part of the Sync object?

                app.sync(
                Sync(id='store_model', property='data', default=None),
                Sync('model', 'value', default='mtl')
                )

                That's not great though, because then the default can't be conditional.

                Alternatively, the Sync property could be something provided in the layout. A new special property:

                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model',
                value=dash.properties.Synced( # <-- A more complex object provided in heretarget_id='store_model',
                target_property='data',
                default='mtl'# <-- This could be dynamic
                )
                # ...
                ),
                dcc.Input(
                id='velocity'value=dash.properties.Synced(
                target_id='store_velocity',
                target_property='data',
                default=''
                )
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])

                In this case, the logic would be:

                1. If the synced component's "target"'s value is undefined (i.e. not specified), then use the default.
                  In this case, the store's data property isn't defined when initializing the first tab, so the dropdown's value is used.
                2. If the synced component's "target"'s value is defined, then use that instead.
                  In this case, when tab 1 is re-selected, the dropdown's value gets populated from the store.

                Alternatively, the default value could be pushed into the dcc.Store:

                app.layout=html.Div([
                dcc.Store(id='store_model', data='mtl'),
                dcc.Store(id='store_velocity', data=0),
                # tabs & content ...
                ])
                @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                ifselected_tab=='controls':
                returnhtml.Div([
                dcc.Dropdown(
                id='model',
                value=dash.properties.Synced(
                id='store_model',
                property='data',
                )
                ),
                dcc.Input(
                id='velocity'value=dash.properties.Synced(
                id='store_velocity',
                property='data'
                )
                )
                ])
                elifselected_tab=='model':
                returnhtml.Div([
                dcc.Graph(
                id='graph-1'
                ),
                ])

                But in this model, the property value couldn't be dynamic.


                Once potentially nice feature of using dcc.Store for this is the ability to save values
                into localstorage across page loads. Lifecycle:

                1. dash.properties.Synced(...) sets a default, dcc.Store(...) is undefined
                2. dcc.Store reads the data from localstorage as part of its component lifecycle and calls setProps
                3. dcc.Dropdown gets updated with that value that just "changed" from the dcc.Store
                4. When user updates the dropdown value, that new value gets synced up with dcc.Store again

                Other Synced applications

                Two-way sync has come up in a few other cases:

                1. dcc.Tabs & dcc.Location - As I switch tabs, the URL could update. If I load a new URL, it could update the tab that is loaded.
                app.layout=html.Div([
                dcc.Location(id='url'),
                dcc.Tabs(
                id='tabs',
                value=dash.properties.Synced(
                id='url',
                property='href'
                ),
                children=[
                dcc.Tab(value='/page-1', label='Analysis 1'),
                dcc.Tab(value='/page-2', label='Analysis 2'),
                ]
                ),
                html.Div(id='output')
                ])
                @app.callback(Output('output', 'children'), [Input('tabs', 'value')])defupdate_output(tab):
                iftab=='/page-1':
                # ...eliftab=='/page-1':
                # ...
                1. Providing a UI that has multiple controls that represent the same number. For example, in this "Rent or Buy" calculator, you can either enter a number or drag a slider:
                  image

                2. Crossfiltering in graphs. We sort of get around this right now by having a collection of "event properties" (selectedData) that can connect to a separate graph's figure. Perhaps this logic would simpler with Synced: dcc.Graph could bubble up figure.data[].selectedids as a top-level property and these could all be synced with each other. When it changes in one graph, it would update across all graphs:

                  app.layout=html.Div([
                  dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
                  dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
                  dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
                  dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('graph-1', 'selectedids')),
                  ])

                  These could even be synced up with a dash_table.DataTable.selected_row_ids:

                  app.layout=html.Div([
                  dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('graph-2', 'selectedids')),
                  dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('graph-3', 'selectedids')),
                  dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('graph-4', 'selectedids')),
                  dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('datatable', 'selectedids')),
                  dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('graph-1', 'selectedids'))
                  ])

                  Writing synced properties in a "chain" like this feels a little clever, perhaps they could just be centrally tied up to a dcc.Store (both syntaxes would work, this version just might be easier to teach):

                  app.layout=html.Div([
                  dcc.Store(id='selected'),
                  dcc.Graph(id='graph-1', selectedids=dash.properties.Synced('selected', 'selectedids')),
                  dcc.Graph(id='graph-2', selectedids=dash.properties.Synced('selected', 'selectedids')),
                  dcc.Graph(id='graph-3', selectedids=dash.properties.Synced('selected', 'selectedids')),
                  dcc.Graph(id='graph-4', selectedids=dash.properties.Synced('selected', 'selectedids')),
                  dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Synced('selected', 'selectedids'))
                  ])

                  In this case, the graphs are crossfiltered simply by the most recent selection, so no custom transformations are needed.

                  In other forms of crossfiltering, the crossfiltering is the union or intersect of actions. Now it feels like we're getting back into layout-embedded clientside transformations as explored in Dash Clientside Transformations dash-renderer#142:

                  app.layout=html.Div([
                  dcc.Store(id='selected', selectedids=union(
                  dash.properties.Input('graph-1', 'selectedids'),
                  dash.properties.Input('graph-2', 'selectedids'),
                  dash.properties.Input('graph-3', 'selectedids'),
                  dash.properties.Input('graph-4', 'selectedids'),
                  dash.properties.Input('datatable', 'selected_row_ids'),
                  )),
                  dcc.Graph(id='graph-1', selectedids=dash.properties.Input('selected', 'selectedids')),
                  dcc.Graph(id='graph-2', selectedids=dash.properties.Input('selected', 'selectedids')),
                  dcc.Graph(id='graph-3', selectedids=dash.properties.Input('selected', 'selectedids')),
                  dcc.Graph(id='graph-4', selectedids=dash.properties.Input('selected', 'selectedids')),
                  dash_table.DataTable(id='datatable', selected_row_ids=dash.properties.Input('selected', 'selectedids'))
                  ])

                  (Transformations might bring up some other issues - exploring these below)

                3. The community has brought this up a few times:


                Some other considerations

                This is about as far as I've thought so far. Here are some other questions & ideas to explore.

                • Data-* properties?
                  With this API, Synced only works with top-level properties.
                  dcc.Store only has a single top-level property for storing data: data
                  So, if you wanted to store 10 inputs, you would need to create 10 dcc.Store components.

                  It'd be nice if you could specify arbitrary properties on a single dcc.Store:

                  dcc.Store(id='store'),
                  dcc.Dropdown(value=Synced('store', 'data-model')),
                  dcc.Input(value=Synced('store', 'data-velocity')),

                  We have special support for aria-* and data-* properties in components, so
                  maybe we could use that?

                • Transformations?
                  Through careful prop design across components, many properties could be 1-1 with each other.
                  However, there might be some synced components that would require a (inverse)transformation.
                  For example, two input components: one in lbs, the other in kgs.
                  We might be able to just allow two callbacks for arbitrary transformations:

                  app.layout=html.Div([
                  dcc.Input(id='lbs'),
                  dcc.Input(id='kg'),
                  ])
                  @app.callback(Output('lbs', 'value'), [Input('kg', 'value')])defkg_to_lbs(value):
                  returnvalue*2.2@app.callback(Output('kg', 'value'), [Input('lbs', 'value')])defkg_to_lbs(value):
                  returnvalue/2.2

                  Of course, if we supported these callback expressions then we wouldn't
                  necessarily need the special layout property dash.properties.Synced, as
                  a user could just write identity callbacks:

                  app.layout=html.Div([
                  dcc.Store(id='selected'),
                  dcc.Graph(id='graph-1'),
                  dcc.Graph(id='graph-2'),
                  dcc.Graph(id='graph-3'),
                  dcc.Graph(id='graph-4'),
                  dash_table.DataTable(id='datatable')
                  ])
                  defidentity(i):
                  returniapp.callback(Output('selected', 'data'), [Input('graph-1', 'selected_data')])(identity)
                  app.callback(Output('selected', 'data'), [Input('graph-2', 'selected_data')])(identity)
                  app.callback(Output('selected', 'data'), [Input('graph-3', 'selected_data')])(identity)
                  app.callback(Output('selected', 'data'), [Input('graph-4', 'selected_data')])(identity)
                  app.callback(Output('selected', 'data'), [Input('datatable', 'selected_row_ids')])(identity)
                  app.callback(Output('graph-1', 'selected_data'), [Input('seleted', 'data')])(identity)
                  app.callback(Output('graph-2', 'selected_data'), [Input('seleted', 'data')])(identity)
                  app.callback(Output('graph-3', 'selected_data'), [Input('seleted', 'data')])(identity)
                  app.callback(Output('graph-4', 'selected_data'), [Input('seleted', 'data')])(identity)
                  app.callback(Output('datatable', 'selected_row_ids'), [Input('seleted', 'data')])(identity)

                  And these could be written with clientside_callbacks too.

                  The union/intersect version of crossfiltering would have a different cyclical transformation:

                  app.layout=html.Div([
                  dcc.Store(id='selected'),
                  dcc.Graph(id='graph-1'),
                  dcc.Graph(id='graph-2'),
                  dcc.Graph(id='graph-3'),
                  dcc.Graph(id='graph-4'),
                  dash_table.DataTable(id='datatable')
                  ])
                  defintersect(*args):
                  returnreduce(np.intersect1d, args)
                  defidentity(i):
                  returniapp.callback(
                  Output('selected', 'data'),
                  [Input('graph-{}'.format(i)) foriinrange(1, 5)] +
                  [Input('datatable', 'selected_row_ids')]
                  )(intersect)
                  app.callback(Output('graph-1', 'selected_data'), [Input('selected', 'data')])(identity)
                  app.callback(Output('graph-2', 'selected_data'), [Input('selected', 'data')])(identity)
                  app.callback(Output('graph-3', 'selected_data'), [Input('selected', 'data')])(identity)
                  app.callback(Output('graph-4', 'selected_data'), [Input('selected', 'data')])(identity)
                  app.callback(Output('datatable', 'selected_row_ids'), [Input('selected', 'data')])(identity)

                  So, dash.properties.Sync would just be a in-layout shorthand for the common case: 1-1, clientside syncing of properties with built-in default handling.

                  Would the two-way @app.callback method be able handle the lifecycle ambiguity mentioned at the top of this post? Let's see:

                  app.layout=html.Div([
                  dcc.Store(id='store_model'),
                  dcc.Store(id='store_velocity'),
                  dcc.Tabs(
                  id='tabs',
                  tabs=[
                  dcc.Tab(label='Controls', value='controls'),
                  dcc.Tab(label='Model', value='model'),
                  ],
                  value='controls'
                  ),
                  html.Div(id='content')
                  ])
                  @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                  ifselected_tab=='controls':
                  returnhtml.Div([
                  dcc.Dropdown(
                  id='model'
                  ),
                  dcc.Input(
                  id='velocity'
                  )
                  ])
                  elifselected_tab=='model':
                  returnhtml.Div([
                  dcc.Graph(
                  id='graph-1'
                  ),
                  ])
                  @app.callback(Output('store_model', 'data'), [Input('model', 'value')], [State('store_model', 'data')])defsync_store_from_dropdown(dropdown_value, existing_store_value):
                  # This callback gets fired when the dropdown value# changes from user interaction _or_ gets rendered.## What we want:# - If store is empty (initial page load), then sync up the store# with the dropdown's value# - If the store isn't empty, then we're at an ambiguous place:# - The user might be on tab 1 and might have just selected a# new dropdown value. In that case, we need to sync.# - But, if this is getting fired because the user just switched# back to tab 1 and the dropdown was just rendered with its# default value (via the callback), then we don't want to sync the dropdown's# default value back to the store, we want the store to populate# the dropdown# So, there is some detail about default values that# cyclical callbacks don't handle.@app.callback(Output('model', 'value'), [Input('store_model', 'data')], [State('model', 'value')])defsync_dropdown_from_store(store_value, existing_data_value):
                  # ...?

                  In this case, our cyclical callbacks aren't handling default values.

                  Perhaps we need a notion of default values & undefined values.

                  Then, our initialization routine would:

                  1. Analyze the cyclical graph and look for default values & non-undefined values
                  2. Start firing callbacks in a circle, starting with non-undefined values, or if they don't exist, default values.
                  3. Firing everything in one complete circle:
                  app.layout=html.Div([
                  dcc.Store(id='store_model'),
                  dcc.Store(id='store_velocity'),
                  dcc.Tabs(
                  id='tabs',
                  tabs=[
                  dcc.Tab(label='Controls', value='controls'),
                  dcc.Tab(label='Model', value='model'),
                  ],
                  value='controls'
                  ),
                  html.Div(id='content')
                  ])
                  @app.callback(Output('content', 'children'), [Input('tabs', 'value')])defupdate_children(selected_tab):
                  ifselected_tab=='controls':
                  returnhtml.Div([
                  dcc.Dropdown(
                  id='model',
                  value=dash.properties.Default('mtl') # <--- provide a default value
                  ),
                  dcc.Input(
                  id='velocity'
                  )
                  ])
                  elifselected_tab=='model':
                  returnhtml.Div([
                  dcc.Graph(
                  id='graph-1'
                  ),
                  ])
                  @app.callback(Output('store_model', 'data'), [Input('model', 'value')])defsync_store_from_dropdown(dropdown_value):
                  returndropdown_value@app.callback(Output('model', 'value'), [Input('store_model', 'data')])defsync_dropdown_from_store(store_value):
                  returnstore_value

                  Lifecycle:

                  1. Page is loaded, store is undefined, tab 1 is selected

                  2. Dropdown is rendered. Analyze the elements in the callback graph:

                    • Store is undefined
                    • Dropdown has a default value

                    So, since dropdown has a default, so fire the dropdown->store callback

                  3. Store is updated. Assume that this callback chain does not "evolve". Assume that firing the store->dropdown callback will not change the value of the dropdown.

                  4. Select the second tab.

                  5. Select & render the first tab. Analyze the elements in the callback graph:

                    • Store is defined
                    • Dropdown has a default value

                    In this case, defined values have a higher priority than default values, so fire the store->dropdown callback.

                  This seems to work...

                  It would probably be worth looking at the lifecycle of the other examples too. For example, in the crossfiltering/intersection example, we might want to initalize selecteddata=[] in all of the elements.

                  In that case (where everything is defined), which callbacks do we fire on initialization?

                  • We could assume that the properties were provided in a "consistent" state and we wouldn't fire them.

                  • Or perhaps we fire all of the callbacks in a circle, so the user could provide something like:

                    app.layout=html.Div([
                    dcc.Store(id='selected'),
                    dcc.Graph(id='graph-1', selected=['a', 'b']),
                    dcc.Graph(id='graph-2', selected=['c', 'b']),
                    dcc.Graph(id='graph-3', selected=['d', 'e']),
                    dcc.Graph(id='graph-4', selected=['e', 'f']),
                    dash_table.DataTable(id='datatable', selected=['a', 'g'])
                    ])

                    in which case, the system would evolve into:

                    • Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                    • Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`
                    • Circle is complete.
                  • If all of the values are defined at start, then it becomes ambiguous where to start

                  app.layout=html.Div([
                  dcc.Store(id='selected', selected=['a', 'b']),
                  dcc.Graph(id='graph-1', selected=['a', 'b']),
                  dcc.Graph(id='graph-2', selected=['c', 'b']),
                  dcc.Graph(id='graph-3', selected=['d', 'e']),
                  dcc.Graph(id='graph-4', selected=['e', 'f']),
                  dash_table.DataTable(id='datatable', selected=['a', 'g'])
                  ])

                  If we start with Graphs->Store, then we get:

                    1. Callback 1 - Graphs->Store: Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
                    1. Callback 2-5 - store->graph-1, store->graph-2, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`

                  But if we start Store->Graphs, then we get:

                    1. store->graph-1, store->graph-2, ...: ['a', 'b']
                    1. graphs->store: ['a', 'b']

                  However, the user provided an "inconsistent" system, so perhaps this is OK.

                  We could also assume that if all of the variables are defined, then the system is "consistent" and we could skip firing the initialization callbacks.

                Metadata

                Metadata

                Assignees

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions