plotly / dash

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

Callback (multi) output not propagated to next Input #1167

Open audetto opened 4 years ago

audetto commented 4 years ago

Describe your context

Python 3.6.6 on Windows 7

dash                 1.9.1
dash-bootstrap-components    0.9.1
dash-core-components 1.8.1
dash-html-components 1.0.2
dash-renderer        1.2.4
dash-table           4.6.1
Windows 7
Chrome 79

Describe the bug

The Output of a callback is not fed as input to the next one

Callback 1: Some input -> Outputs A B C D Callback 2: Input B C -> Output E Callback 3: Input A B C D E -> Output F

B: dbc.Select C: dcc.Slider E: dbc.Input

The problem is that Callback 3 never sees the updated E output of 2, even though it is definitely computed. Moreover, the GUI (E) is not updated either. But if I trigger manually C, then all works correctly again.

I added full logging and I see Callback 2 called multiple times with different triggers and the correct calculation is performed, but just it never used. I thought at the begin that Callback 2 was called with stale input, but it is not the case. It is called correctly, but its results seems to be discarded.

Expected behavior

At some point Callback 3 should be called with the new output of E. In my example it is triggered multiple times, but never with the new output E.

Screenshots

This happens at work. Cant take anything out of it.

alexcjohnson commented 4 years ago

1103 fixes a number of similar bugs. We'll merge this soon and release it in the next dash version - we can leave this issue open, but then once dash 1.10 is out I'll ask you to check whether the problem persists.

narly-david commented 4 years ago

👍 Can confirm this issue. Current setup involves using a hidden div to store data to feed into several child components (where the hidden div is the only input).

The callback correctly calls the update function, the update function correctly returns the value to store in it's div. However, the UI does not update.

narly-david commented 4 years ago

Looks like this is still an issue in 1.10.

alexcjohnson commented 4 years ago

Ah sorry, #1103 did not get merged yet but will soon, so let's check again in 1.11. That shouldn't be more than a week or so 😄

audetto commented 4 years ago

Tried 1.11 and things have changed

1) GOOD: all callbacks seem to be called after GUI interaction 2) GOOD: dcc.Loading() show the loading indicator even in the initialisation phase 3) BAD: not all initial callbacks are triggered

As usual I do not have an example but I seem to have found a pattern

UI, an input A has some value property B set to something in the layout A callback X triggered from a button C has output Output(A, B) Another callback Y has this input Input(A, B)

So the logic is that either the user enters the value directly in the UI or clicks some button which populates it.

Now callback X is called, but because n_clicks is 0 it returns PreventUpdate. Callback Y is never called.

If I do not register X, then Y is called.

Am I doing the right thing? Not sure, before it worked. How is the initialisation phase supposed to work?

alexcjohnson commented 4 years ago

Now callback X is called, but because n_clicks is 0 it returns PreventUpdate. Callback Y is never called.

@audetto yes, that's actually as intended - all callbacks get an initial call, except if all the inputs to the callback are themselves outputs of some other callback and all of those outputs get prevented. Prior to v1.11 this was not handled entirely consistently, but that is how it has been handled since before v1.0: https://github.com/plotly/dash/blob/3ec55b5a97fdd34ed8384085df884d86730b1a9c/tests/integration/test_integration.py#L158-L160

So knowing that, you can either have your callback X not PreventUpdate (give it the current value as State and just return it, perhaps), or give callback Y another input that's unused except to provide an initial trigger.

audetto commented 4 years ago

I have added the dummy Input to trigger it, but it feels a weak solution which could break at any time when I add another callback in a different place.

Dash could very well simulate the dummy input by ensuring all callbacks are triggered initially, exactly as if they all depended on the dummy input.

What I think would help me a lot is the exact rule used by dash to traverse the dependencies, which I guess is a lot less trivial that I originally thought, with so many more corner cases (for dash) to consider.

alexcjohnson commented 4 years ago

Dash could very well simulate the dummy input by ensuring all callbacks are triggered initially, exactly as if they all depended on the dummy input.

Apologies for any confusion this has generated! In fact as I was refactoring the callback pipeline for #1103 initially it worked as you were hoping. But seeing that the above test failed, this was a corner case I had to add a fix for explicitly. Our goal was to ensure backward compatibility with every known expected case, while fixing cases we considered bugs.

I agree that this one has arguments either way, but since we already had an explicit test this seems pretty clearly the expected interpretation. The main argument in its favor is I think a variant of your worry about adding more callbacks: if you have one callback that's calling PreventUpdate and only later add another one that flows directly from it, it's reasonable to expect that the PreventUpdate would halt the whole callback chain initially since that's what it does later on.

What I think would help me a lot is the exact rule used by dash to traverse the dependencies

That's a good idea - we should describe these details in the docs. Here's a start - probably needs a bit of elaboration to be at level we need but I think it's complete in the logic:

chriddyp commented 4 years ago

That's a good idea - we should describe these details in the docs

(Good idea - Here's an issue: https://github.com/plotly/dash-docs/issues/852. I'll work on adding this in the coming weeks when I make my way over to the callback docs.)