-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ideas & discussion on storing data across tabs, synced items, & cyclical callbacks #844
Comments
I believe I have got an equivalent problem working in a Dash instance. I have a multi-tab layout, but I have put it in a Div, and inside that Div I have 2x dcc.Location url-reader and url-writer, a Store(global-store), and a set of Store('tab'-store), plus the Tabs. Each Tab is then a dynamic layout. The key is that all the Stores are scoped outside of Tabs. From each tab, when I get values to save (from dcc.Dropdown values etc.) I save into the Output('tab'-store). I have a callback for Input('tab'-store) (x number of tabs), State(global-store), Output(global-store) which basically creates a Dict of {'tabname': 'tab'-store.value} which is updated for any change. Then on any of my Tabs I can reference the State(global-store), which lets me transfer data between tabs. The 2x dcc.Locations also allow me to update the url-writer from the global-store - so the URL provides a 'deep-link' that can select the tab and initialise the widgets on the tab with the right values, which updates the browser URL for every change (via the store), and the url-reader is watched for Input which is when someone follows / enters a deep-link, and it is the final Input to the global-store. (The cb has to check if url-reader is the ctx.triggered source, and if so update the correct part of the global-store) Hope that all makes sense. I feel that this is a pattern that is really useful for all Tabbed Dashboards - but if its 'known' I certainly didn't find it anywhere from browsing. |
Do you happen to have an example of how you did this ncorran? |
Fix string comparison for lint:black in package.json
@chriddyp Thank you for your post. |
Did you manage to get anywhere with this?
I do have a working dashboard following this approach, with both global
context, and parameterised URL reading/writing to initialise the context if
'deep linking' into a tab.
…-Nick
On Mon, May 17, 2021 at 9:16 PM Brad Lanning ***@***.***> wrote:
Do you happen to have an example of how you did this ncorran?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#844 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AIJIS333NPYQZCTH7DDL6JTTOF2STANCNFSM4IIB22NA>
.
|
Fix string comparison for lint:black in package.json
Can close
…On Thu, Jul 18, 2024, 8:32 AM Greg Wilson ***@***.***> wrote:
@chriddyp <https://github.com/chriddyp> is this one still relevant or has
it gone stale? thanks - @gvwilson <https://github.com/gvwilson>
—
Reply to this email directly, view it on GitHub
<#844 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABGISRKXEI2OSPSWLEUSTITZM7GYXAVCNFSM6AAAAABLC2AG3WVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMZWG4ZDINJYGU>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
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:
This is how users are currently writing these applications (this example does not work):
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
One challenge with this approach is the initialization logic could be ambiguous.
Here's the lifecycle in this app:
dcc.Store(id='store_model')
is initialized anddcc.Dropdown(id='model', value='mtl')
isn't renderedvalue='mtl'
) and so one could argue thatdcc.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.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?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:In this case, the logic would be:
In this case, the store's
data
property isn't defined when initializing the first tab, so the dropdown's value is used.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
: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 valuesinto localstorage across page loads. Lifecycle:
dash.properties.Synced(...)
sets a default,dcc.Store(...)
is undefineddcc.Store
reads the data from localstorage as part of its component lifecycle and callssetProps
dcc.Dropdown
gets updated with that value that just "changed" from thedcc.Store
dcc.Store
againOther
Synced
applicationsTwo-way sync has come up in a few other cases:
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.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:
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'sfigure
. Perhaps this logic would simpler withSynced
:dcc.Graph
could bubble upfigure.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:These could even be synced up with a
dash_table.DataTable.selected_row_ids
: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):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:
(Transformations might bring up some other issues - exploring these below)
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
:We have special support for
aria-*
anddata-*
properties in components, somaybe 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:
Of course, if we supported these callback expressions then we wouldn't
necessarily need the special layout property
dash.properties.Synced
, asa user could just write identity callbacks:
And these could be written with
clientside_callbacks
too.The union/intersect version of crossfiltering would have a different cyclical transformation:
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: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:
Lifecycle:
Page is loaded, store is undefined, tab 1 is selected
Dropdown is rendered. Analyze the elements in the callback graph:
So, since dropdown has a default, so fire the
dropdown->store
callbackStore 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.Select the second tab.
Select & render the first tab. Analyze the elements in the callback graph:
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:
in which case, the system would evolve into:
Graphs->Store
:Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
store->graph-1
,store->graph-2
, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`If all of the values are defined at start, then it becomes ambiguous where to start
If we start with
Graphs->Store
, then we get:Graphs->Store
:Store.data = union(...) = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
store->graph-1
,store->graph-2
, ...: ['a', 'b', 'c', 'd', 'e', 'f', 'g']`But if we start
Store->Graphs
, then we get:store->graph-1
,store->graph-2
, ...:['a', 'b']
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.
The text was updated successfully, but these errors were encountered: