plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.
https://plotly.com/dash
MIT License
21.15k stars 2.04k forks source link

support circular dependencies #889

Open alexcjohnson opened 5 years ago

alexcjohnson commented 5 years ago

Circular deps keep coming up. Most of the use cases I've seen are fairly simple, like 2 alternate ways to accomplish the same task, that you want to keep in sync. For example today on the community board someone asked the perfectly reasonable question of how to make the selected tab reflected in the url, and vice versa so you could load your app with the desired tab open. https://community.plot.ly/t/combining-dcc-tabs-and-dcc-location/27831

The code they tried seems like the natural way to do it, but fails as it requires a circular dependency between Location.pathname and Tabs.value. I don't see a simple alternative. We could make one using patterns we've used elsewhere (like adding a *_timestamp prop to one of these components, or having Location.pathname update href and vice versa, which it should arguably do) but this feels hacky and fragile - it's making an explicit circular ref into an implicit one that may make infinite loops that are hard to debug and even harder to fix.


My proposal: create a new dash.dependencies class, tentatively CircularOutput, by which users could specify that this particular output may be in a circular dependency chain. Then in the renderer we would:

rpkyle commented 5 years ago

This would be useful anyway to show with the stack trace when debugging errors.

This is an interesting idea; how would this look in practice? Would each callback’s props and their values optionally appear in the stack trace — just before the sequence of calls which follows them?

alexcjohnson commented 5 years ago

Would each callback’s props and their values optionally appear in the stack trace — just before the sequence of calls which follows them?

Something like that - probably in an expandable section. I think we should be able to manage all this purely on the front end, without needing to send anything new (or make any code changes) to the back ends.

chriddyp commented 5 years ago

👍 . There is also a case for using circular dependencies to sync up dcc.Store with controls so that you can access the the control values (via dcc.Store) in separate tabs or pages. It'd be circular so that when you'd render the initial page, dcc.Store would sync the value back to the original component.

There's another use case around crossfiltering - with the right property design, syncing or providing the union of selected points could be a lot simpler.

I wrote about some of these use cases in https://github.com/plotly/dash/issues/844.

alexcjohnson commented 5 years ago

Ah thanks @chriddyp - I forgot about #844, really nice collection of use cases. We could merge the two issues if you like, though TBH it might be easier to leave that one as a smörgåsbord to pick and choose from and have the discussion over here.

I feel like Synced covers a number of common use cases, but a will hit a wall of real-world complexity it'll be hard to get past; whereas if we just extend the existing callback formalism we naturally have access to its full power. Some of these you mentioned in #884:

But also:

alexcjohnson commented 5 years ago

Prevent unchanged values from dispatching callbacks. I can't think of a reason ever to allow this, anyone want to argue with that? See #883

Just came across a use case for this - extendData for graphs. https://github.com/plotly/dash-core-components/pull/621/files#r318682728 - this is a funny one, it's a prop that really just interfaces to an imperative command for adding data to a graph incrementally. So if we were to do this we would need to find a way to exclude extendData, and see if there are any other props in this mold.

Actually, perhaps there's an easy solution at the component level: clear extendData (setProps({extendData: null})?) once it has been incorporated in the figure. Hopefully nobody is listening to extendData as an input, that would be weird... but perhaps it would even still work, they'd just get the new data and then another call with null.

Seems like that approach would work for other imperative props, as long as there's a suitable noop value to assign it (could be null but not necessarily; you could have a multiplyBy prop whose noop value is 1 for example)

kylemcmearty commented 4 years ago

@chriddyp "There is also a case for using circular dependencies to sync up dcc.Store with controls so that you can access the the control values (via dcc.Store) in separate tabs or pages. It'd be circular so that when you'd render the initial page, dcc.Store would sync the value back to the original component."

Has there been any updates on circular dependencies and dcc.store? I have an input value / slider (A) that I use to calculate a dataframe and store that data into a dcc.store. The problem is that I want to use the dataframe from dcc.store in a callback that uses input (A) as an Output, thus creating a circular dependency. Is this something at all related to what you were mentioning about syncing up dcc.store with controls?

sdementen commented 3 years ago

In order to allow cycles in the graph yet avoid infinite loops in the callbacks, could the following "marking" solution work ?

I haven't tested the logic but I could do it (in python with synthetic dependency graphs) if needed. Did you already try something in this vein ?

emilhe commented 3 years ago

I would also be very interested in a generic solution :). Until then, i have made a custom Monitor component, which makes it possible to lift state into the monitor, thereby avoiding the circular dependency error. Here is an example of keeping a Slider and an Input component in sync using dash-extensions==0.0.33,

import dash_core_components as dcc
from dash import Dash
from dash.dependencies import Input, Output
from dash_extensions import Monitor

app = Dash()
app.layout = Monitor([
    dcc.Input(id="input", autoComplete="off", type="number", min=0, max=100, value=50),
    dcc.Slider(id="slider", min=0, max=100, value=50)],
    probes=dict(probe=[dict(id="input", prop="value"), dict(id="slider", prop="value")]), id="monitor")

@app.callback([Output("input", "value"), Output("slider", "value")], [Input("monitor", "data")])
def sync(data):
    probe = data["probe"]
    return probe["value"], probe["value"]

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

The syntax is more involved than a native solution though, and only components in the Monitor component tree can be monitored. As all of there children are visited at each render, performance might also become an issue at some point.

rusiano commented 3 years ago

+1. I am also interested in this.

chadaeschliman commented 3 years ago

What about a much more limited approach:

For something like a linked slider and input, the paradigm would be to pass both values as Inputs and Outputs and use callback_context to determine which value to propagate to the outputs. More complicated examples like length/width/aspect_ratio would involve more Inputs and Outputs but would follow the same paradigm.

The advantages to this approach are:

romanodev commented 3 years ago

I would be very interested in it as well. My use case is the following:

I run python code in a container and, as a result, I get an iterator that gives standard output as it comes. I need to visualize this stream into the app. I am trying to do this via two callbacks that call each other with this iterator (not hashable) being a global variable. Maybe there is a better way to do this.

UPDATE: I found the answer here: https://community.plotly.com/t/how-to-turn-off-interval-event/5565/7