Callback cascade (A -> [B, C] and B -> C) not possible?

Hi all,

Imagine two dcc.Dropdown components:

  • Country: Displays a bunch of countries, e.g. [“France”, “Germany”]
  • City: Displays a bunch of cities depending on what’s selected in Country. If you select “France” then the city list shows [“Paris”, “Marseille”], and when you select “Germany” it displays [“Berlin”, “Hamburg”].

So far, so good. Setting up a callback that updates the City list based on the selected Country is trivial.

The problem arises when I want to additionally control both Country and City via a third input upstream, e.g. the URL (think foo.com?country=Germany&city=Hamburg). Then we have the following dependency graphs:

  1. URL -> [Country, City]
  2. Country -> City

In an ideal world we could implement this as two independent callbacks, but that’s not possible according to the FAQ:

https://dash.plot.ly/faqs (“A component/property pair can only be the Output of one callback”)

So, given that City is an output of both callback 1 and 2 we need to merge both callbacks into one. That is, however, impossible because Country would have to be both an input and an output of the callback, which is not allowed.

Any ideas on how I can make this work?

Maybe you could organise your callbacks differently to something like

  1. URL -> Country
  2. [URL, Country] -> City

Possibly also relevant would be dash.callback_context which would let you figure out which input triggered the second callback. You can read about that here under the question “How do I determine which Input has changed?

1 Like

Thanks, but I can’t re-organize that way, because I really need the URL to be able to control both Country and City.

The callback context doesn’t help me here because dash won’t allow me to set up a callback with Country being both the input and the output of a callback in the first place.

I’ve wanted to do a similar thing, thus far I’ve only been able to work around it by using either the inputs on the page or the URL interchangeably.

The URL sends me directly to the output page, but the inputs remain blank; I can then go and fill the inputs in if I want to change the parameters.

I know this isn’t what you’re looking for, but it’s the closest I’ve been able to get.

I do this all the time with the method outlined here https://github.com/plotly/dash/issues/188#issuecomment-360313359

The question isn’t about saving state to the URL and back but of the Country component which should but cannot be both an input and an output.

Think about it this way. Here is a list of features that the gist demonstrates:

  1. read input from URL params and set component values
  2. when component changes, update the URL
  3. allows callbacks to change component values

So take the gist and delete item 2 and you have your solution.

Here you go

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from urllib.parse import urlparse, parse_qsl, urlencode

app = dash.Dash()

app.config.suppress_callback_exceptions = True

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-layout')
])


def apply_default_value(params):
    def wrapper(func):
        def apply_value(*args, **kwargs):
            if 'id' in kwargs and kwargs['id'] in params:
                kwargs['value'] = params[kwargs['id']]
            return func(*args, **kwargs)

        return apply_value

    return wrapper


def build_layout(params):
    layout = [
        apply_default_value(params)(dcc.Dropdown)(
            id='country',
            options=[{'label': i, 'value': i} for i in ['Australia', 'New Zealand']],
            value='Australia'
        ),
        apply_default_value(params)(dcc.Dropdown)(
            id='cities',
            options=[],
            # value='LA'
        ),
    ]

    return layout


def parse_state(url):
    parse_result = urlparse(url)
    params = parse_qsl(parse_result.query)
    state = dict(params)
    return state


@app.callback(Output('page-layout', 'children'),
              inputs=[Input('url', 'href')])
def page_load(href):
    if not href:
        return []
    state = parse_state(href)
    return build_layout(state)


component_ids = [
    'country',
    'cities',
]


@app.callback(Output('url', 'search'),
              inputs=[Input(i, 'value') for i in component_ids])
def update_url_state(*values):
    state = urlencode(dict(zip(component_ids, values)))
    return f'?{state}'


@app.callback(Output('cities', 'options'),
              inputs=[Input('country', 'value')])
def update_cities(country):
    if country == "Australia":
        return [{'label': i, 'value': i} for i in ['Sydney', 'Melbourne']]
    elif country == "New Zealand":
        return [{'label': i, 'value': i} for i in ['Auckland', 'Wellington']]
    else:
        raise Exception


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

@sjtrny I have to apologize, this is indeed a viable solution! Thanks so much :slight_smile:

P.S: I think the two functions wrapper and apply_value can be merged into one, no?

I was just thinking about this method this morning, beat me to it! Appreciate the example too!