Chained callbacks and dropdowns

Hi everyone,

I’m quite new using Dash and plotly and I’m facing a problem i can’t find any solution.
I’ve done everything like in this tutorial : https://dash.plot.ly/getting-started-part-2 but somehow it does not work exactly the same.

Let me explain what I’m dealing with.

I want to do a dashboard that plots a funnel for a website selected in a first dropdown menu, then once this website is chosen I have a second dropdown menu to select a product (this list of products depends on the website). I also have a datepickerrange but this part is not useful for the problem I’m facing right now.

The function tunnel() is a function I created that generates data needed for the funnel chart (SQL query, cleaning, …).When a website is chosen it will generate a dataframe with the funnel data for each products available. This is why I have a second dropdown menu, to select a specific product in this dataframe.

My app “works” but when I’m selecting a new website (rather than the one per default), the list of available products is not updated and the graph is not displayed anymore.
But if I click again on the website then suddenly my list of available products is updated and the funnel chart is displayed.

Does anyone know how could I solve this ?
(In the code below you’ll see I used global df which isn’t safe I know it now since I just read the part 6 of the tutorial but I’d like to deal with that after my dropwdowns issues).

Here is my code :

app = dash.Dash()

res_ad = False

# Website choice
site = "site1"

site_name = {"site1": "Site A",
			"site2": "Site B"
             }


end = date.today()
start = end - timedelta(days=30)


df = tunnel(site, start, end, 0.25, res_ad)

# Dropdown menu for websites :
opts_site = [{'label': str(site_name[i]), 'value': str(i)} for i in site_name.keys()]

fig = go.Figure()
fig.add_trace(go.Funnel(
    name="Produit",
    orientation="h",
    y=list(df.loc[df.produit == df.produit.unique()[0], "step"].values),
    x=list(df.loc[df.produit == df.produit.unique()[0], "cumsum"].values),
    textinfo="value+percent initial"
))


graph = dcc.Graph(id='plot', figure=fig, style={"height": "100vh", "width": "95vw"})

app.layout = html.Div([
    html.Div([
        html.H1(children="Funnel",
                style={
                    'textAlign': 'center',
                }),
    ]),
    html.P([
        html.Label("Website choice"),
        dcc.Dropdown(
            id='sites',
            options=opts_site,
            value=site)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Product choice"),
        dcc.Dropdown(id='produits')
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'padding-left': '100px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Date picker"),
        dcc.DatePickerRange(
            id='my-date-picker-range',
            min_date_allowed=date(2019, 1, 1),
            max_date_allowed=end,
            initial_visible_month=end,
            display_format="D MMM YYYY",
            start_date=start,
            end_date=end)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    graph])


@app.callback(Output('produits', 'options'),
              [Input('sites', 'value')])
def update_produits_options(website):
    return [{'label': str(i), 'value': str(i)} for i in df.produit.unique()]


@app.callback(Output('produits', 'value'),
              [Input('produits', 'options')])
def set_produits_value(prod):
    return prod[0]['value']


@app.callback(Output('plot', 'figure'),
              [Input('sites', 'value'),
               Input('produits', 'value'),
               Input('my-date-picker-range', 'start_date'),
               Input('my-date-picker-range', 'end_date')])
def update_figure(website, prod, start_dt, end_dt):
    global df
    start_dt = pd.to_datetime(start_dt).date()
    end_dt = pd.to_datetime(end_dt).date()
    df = tunnel(website, start_dt, end_dt, 0.25, res_ad)
    trace = go.Funnel(
        name=website,
        orientation="h",
        y=list(df.loc[df.produit == prod, "step"].values),
        x=list(df.loc[df.produit == prod, "cumsum"].values),
        textinfo="value+percent initial"
    )
    fig = go.Figure(data=trace)
    return fig

I hope I’ve been clear enough, if not don’t hesitate to ask me questions.
Thanks

Please provide a working sample of your code. You are missing the necessary imports and the tunnel() function is not included - please add a stub function that returns data so the code executes. This was, folks can spend time trying to figure out your problem.

Thanks for answering, sorry here’s a full working code :

import dash
import dash_core_components as dcc
import dash_html_components as html

from plotly import graph_objects as go
from dash.dependencies import Input, Output
import pandas as pd
from datetime import date
from datetime import timedelta

def tunnel(base, start_date, end_date, hyp, res):
    import pandas as pd
    import numpy as np
    if base == "site1":
        df = pd.DataFrame(data={
			"produit": ["product1", "product1", "product1", "product2", "product2", "product2", "product2"],
			"step": ["step a", "step b", "step c", "step d", "step e", "step f", "step g"],
			"cumsum": [45, 40, 30, 80, 50, 41, 24]
		})
    else:
        df = pd.DataFrame(data={
			"produit": ["product3", "product3", "product3", "product4", "product4", "product4"],
			"step": ["step A", "step B", "step C", "step D", "step E", "step F"],
			"cumsum": [200, 150, 100, 200, 100, 50]
		})

    return df

app = dash.Dash()

res_ad = False

# Website choice
site = "site1"

site_name = {"site1": "Site A",
			"site2": "Site B"
             }


end = date.today()
start = end - timedelta(days=30)


df = tunnel(site, start, end, 0.25, res_ad)

# Dropdown menu for websites :
opts_site = [{'label': str(site_name[i]), 'value': str(i)} for i in site_name.keys()]

fig = go.Figure()
fig.add_trace(go.Funnel(
    name="Produit",
    orientation="h",
    y=list(df.loc[df.produit == df.produit.unique()[0], "step"].values),
    x=list(df.loc[df.produit == df.produit.unique()[0], "cumsum"].values),
    textinfo="value+percent initial"
))


graph = dcc.Graph(id='plot', figure=fig, style={"height": "100vh", "width": "95vw"})

app.layout = html.Div([
    html.Div([
        html.H1(children="Funnel",
                style={
                    'textAlign': 'center',
                }),
    ]),
    html.P([
        html.Label("Website choice"),
        dcc.Dropdown(
            id='sites',
            options=opts_site,
            value=site)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Product choice"),
        dcc.Dropdown(id='produits')
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'padding-left': '100px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Date picker"),
        dcc.DatePickerRange(
            id='my-date-picker-range',
            min_date_allowed=date(2019, 1, 1),
            max_date_allowed=end,
            initial_visible_month=end,
            display_format="D MMM YYYY",
            start_date=start,
            end_date=end)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    graph])


@app.callback(Output('produits', 'options'),
              [Input('sites', 'value')])
def update_produits_options(website):
    return [{'label': str(i), 'value': str(i)} for i in df.produit.unique()]


@app.callback(Output('produits', 'value'),
              [Input('produits', 'options')])
def set_produits_value(prod):
    return prod[0]['value']


@app.callback(Output('plot', 'figure'),
              [Input('sites', 'value'),
               Input('produits', 'value'),
               Input('my-date-picker-range', 'start_date'),
               Input('my-date-picker-range', 'end_date')])
def update_figure(website, prod, start_dt, end_dt):
    global df
    start_dt = pd.to_datetime(start_dt).date()
    end_dt = pd.to_datetime(end_dt).date()
    df = tunnel(website, start_dt, end_dt, 0.25, res_ad)
    trace = go.Funnel(
        name=website,
        orientation="h",
        y=list(df.loc[df.produit == prod, "step"].values),
        x=list(df.loc[df.produit == prod, "cumsum"].values),
        textinfo="value+percent initial"
    )
    fig = go.Figure(data=trace)
    return fig


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

Ok. Thanks! Here is what I did to make it work in the way I think you desire (i.e. you select website, that triggers update to options on product dropdown, which in turn updates graph).

Assumptions:

  • You are using the most recent version of Dash!

Here are my tweaks explained:

  • Modified tunnel definition since only base input was used.
  • Removed set_produits_value callback and moved its output to update_produits_value callback
  • In theupdate_figure callback, I reduced the inputs to 1 - that being the only input that should trigger the callback which is the user selecting a product. All other required parameters passed in via State.
  • Added error checking to update_figure to ensure code is not executed unless prod value is set.

Suggestions:

  • Check out dcc.Store in order to eliminate your need for global variables
  • If a change to the date/time will eventually trigger an graph update, add Input('my-date-picker-range', 'date') to the Input list in the update_figure callback.

Hope this helps!

import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
from plotly import graph_objects as go
from dash.dependencies import Input, Output, State
import pandas as pd
from datetime import date
from datetime import timedelta
from dash.dash import no_update


def tunnel(base):
    if base == "site1":
        return pd.DataFrame(data={
            "produit": ["product1", "product1", "product1", "product2", "product2", "product2", "product2"],
            "step": ["step a", "step b", "step c", "step d", "step e", "step f", "step g"],
            "cumsum": [45, 40, 30, 80, 50, 41, 24]
        })
    else:
        return pd.DataFrame(data={
            "produit": ["product3", "product3", "product3", "product4", "product4", "product4"],
            "step": ["step A", "step B", "step C", "step D", "step E", "step F"],
            "cumsum": [200, 150, 100, 200, 100, 50]
        })


app = dash.Dash(__name__)

res_ad = False

# Website choice
site = "site1"

site_name = {"site1": "Site A",
             "site2": "Site B"
             }

end = date.today()
start = end - timedelta(days=30)

df = tunnel(site)

# Dropdown menu for websites :
opts_site = [{'label': str(site_name[i]), 'value': str(i)} for i in site_name.keys()]

fig = go.Figure()
fig.add_trace(go.Funnel(
    name="Produit",
    orientation="h",
    y=list(df.loc[df.produit == df.produit.unique()[0], "step"].values),
    x=list(df.loc[df.produit == df.produit.unique()[0], "cumsum"].values),
    textinfo="value+percent initial"
))

graph = dcc.Graph(id='plot', figure=fig, style={"height": "100vh", "width": "95vw"})

app.layout = html.Div([
    html.Div([
        html.H1(children="Funnel",
                style={
                    'textAlign': 'center',
                }),
    ]),
    html.P([
        html.Label("Website choice"),
        dcc.Dropdown(
            id='sites',
            options=opts_site,
            value=site)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Product choice"),
        dcc.Dropdown(id='produits')
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'padding-left': '100px',
            'display': 'inline-block'}
    ),
    html.P([
        html.Label("Date picker"),
        dcc.DatePickerRange(
            id='my-date-picker-range',
            min_date_allowed=date(2019, 1, 1),
            max_date_allowed=end,
            initial_visible_month=end,
            display_format="D MMM YYYY",
            start_date=start,
            end_date=end)
    ],
        style={
            'textAlign': 'center',
            'width': '400px',
            'fontSize': '20px',
            'display': 'inline-block'}
    ),
    graph])


@app.callback(output=[Output('produits', 'options'),
                      Output('produits', 'value')],
              inputs=[Input('sites', 'value')])
def update_produits_options(website):
    df = tunnel(website)
    return [{'label': str(i), 'value': str(i)} for i in df.produit.unique()], df.produit.unique()[0]


@app.callback(output=Output('plot', 'figure'),
              inputs=[Input('produits', 'value')],
              state=[State('sites', 'value'),
                     State('my-date-picker-range', 'start_date'),
                     State('my-date-picker-range', 'end_date')])
def update_figure(prod, website, start_dt, end_dt):
    if not prod:
        return no_update

    start_dt = pd.to_datetime(start_dt).date()
    end_dt = pd.to_datetime(end_dt).date()
    df = tunnel(website)
    
    trace = go.Funnel(
        name=website,
        orientation="h",
        y=list(df.loc[df.produit == prod, "step"].values),
        x=list(df.loc[df.produit == prod, "cumsum"].values),
        textinfo="value+percent initial"
    )
    return go.Figure(data=trace)


if __name__ == '__main__':
    app.run_server(port=4996, debug=True)
2 Likes

Thanks a lot ! It’s exactly what I wanted to achieve !
I was able to adjust it to my real tunnel() function and I added two inputs in the update_produits_options since when I change the start date or end date it’s possible that a product will not be available anymore.

@app.callback(output=[Output('produits', 'options'),
                      Output('produits', 'value')],
              inputs=[Input('sites', 'value'),
                      Input('my-date-picker-range', 'start_date'),
                      Input('my-date-picker-range', 'end_date')])
def update_produits_options(website, start, end):
    df = tunnel(website, start, end, 0.25, False)
    return [{'label': str(i), 'value': str(i)} for i in df.produit.unique()], df.produit.unique()[0]

Concerning the update_figure, can you explain me the difference when using:

State('my-date-picker-range', 'start_date'),
State('my-date-picker-range', 'end_date')

and:

Input('my-date-picker-range', 'start_date'),
Input('my-date-picker-range', 'end_date')

I’m not sure to get it and I would like to understand.
I used Input because changing the start date or end date will change the numbers of visitors hence affecting my graph funnel. Using State, would it still be the case ?
By the way with your solution I don’t need the global df anymore :+1:

Thanks again.

Basically, Inputs trigger callbacks, States do not. Passing a component’s parameter via State makes it visibile within your callback. Only include parameters in Input which should fire the callback.

So, when I got your code working, I removed the date picker stuff from the Input soley to ensure it wouldn’t trigger the callback. It appears they need to be back in Inputs as you desire their change to fire the callback.

And yes, you don’t need the global ref anymore since you are calling the tunnel function on each update.

1 Like