How to use the same component id twice as output with callback_context?

Hello everyone,
I have a problem getting the same output in two places. I used the callback_contex to do that. but the second function I have defined in my code is not working.
Here is my case:
when I select a csv file from the 1st dropdown, it returns the column names as the options for the 2nd dropdown, and plots a 3d figure using the csv file.
Then, by choosing from 2nd dropdown, a rangeslider generates. I want to update the same 3d plot by moving the rangeslider.

Here is my code,
I appreciate in advance for any feedback and suggestions.

path = 'The address to the csv files'

# call back to store and share the df between callbacks
@callback(
    Output('table1', 'data'),
    Input('dropdown1', 'value'),
    prevent_initial_call=True
)
def store_df(chosen_par):
    if chosen_par is None:
        return dash.no_update
    else:
        df = pd.read_csv(path + chosen_par + '.csv')
        df_json = df.to_json(date_format='iso', orient='split')
    return df_json


# it gets csv files from dropdown1 and returns the feature options in dropdown2
@callback(
    Output('dropdown2', 'options'),
    Input('table1', 'data'),
    prevent_initial_call=True
)
def set_well_options(js_df):
    if js_df is None:
        return dash.no_update
    else:
        dff = pd.read_json(js_df, orient='split')
        features = np.sort(dff.columns.values[2:])
        return features

# it gets the value from dropdown2 and updates the rangeslider
@callback(
    Output('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def set_rangeslider_values(js_df, feature):
    if (feature == []) or (feature is None):
        return dash.no_update
    else:
        dff = pd.read_json(js_df, orient='split')
        min_value = dff[feature].min()
        max_value = dff[feature].max()
    return [min_value, max_value]


@callback(
    Output('graph1', 'figure'),
    Input('dropdown1', 'value'),
    Input('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('my_rangeslider', 'value'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def group1(b1, b2, js_df, ranges, feature):
    triggered_id = ctx.triggered_id
    if triggered_id == 'dropdown1':
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
        return update_with_rangeslider(js_df, ranges, feature)

def plot3d(js_df):
    dff = pd.read_json(js_df, orient='split')
    fig = go.Figure()
    fig.add_trace(go.Scatter3d(x=dff['X'], y=dff['Y'], z=dff['Z'], mode='markers', marker={'color': '#F3CAD2', 'size': 4}))
    return fig

def update_with_rangeslider(js_df, ranges, feature):
    if (feature is None) or (ranges == []):
        return dash.no_update
    else:
        dff = pd.read_json(js_df, orient='split')
        df_sliced_min = dff[(dff[feature] < ranges[0])]
        df_sliced_max = dff[(dff[feature] > ranges[1])]

        fig = go.Figure()
        fig.add_trace(go.Scatter3d(x=df_sliced_min['X'], y=df_sliced_min['Y'], z=df_sliced_min['Z'], mode='markers', marker={'color': '#1F7BAA', 'size': 6}))
        fig.add_trace(go.Scatter3d(x=df_sliced_max['X'], y=df_sliced_max['Y'], z=df_sliced_max['Z'], mode='markers', marker={'color': '#FF206E', 'size': 6}))

        return fig

Hello @Peyman,

Typically, I would define functions before they are going to be called, maybe try shifting the last two functions above the callback.

@jinnyzor
Thanks for the reply. It still works the same. the plot3d function works well, but the other function update_with_rangeslider is not updating the figure.

It looks like you only have 4 arguments from callback, but you have 5 arguments for the function. Thus feature is always None, which forces it to return dash.no_update.

@jinnyzor
I corrected that. I think I missed one input during moving my code here.

right now, In that function, I have two similar inputs Input(‘my_rangeslider’, ‘value’),.
the first one is to trigger the update_with_rangeslider() and the second one is to be used in as an input inside my function.
Is the way I have written right? Thank you

Hello @Peyman,

I’ve not thought of using it that way.

You don’t need to have it twice, the value will be there from the first one. That might be causing your issue.

Thank you again, I did like the below. it prints(ranges) but I don’t understand why the trace does not add to the figure. I guess right now there is a problem with the fig. when I run it, it does not raise any errors and does not show the updates.

@callback(
    Output('graph1', 'figure'),
    Input('dropdown1', 'value'),
    Input('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def group1(b1, b2, js_df, feature):
    triggered_id = ctx.triggered_id
    if triggered_id == 'dropdown1':
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
        return update_with_rangeslider(js_df, feature, b2)

def plot3d(js_df):
    dff = pd.read_json(js_df, orient='split')
    fig = go.Figure()
    fig.add_trace(go.Scatter3d(x=dff['X'], y=dff['Y'], z=dff['Z'], mode='markers', marker={'color': '#F3CAD2', 'size': 4}))
    return fig

def update_with_rangeslider(js_df, feature, ranges):
    if (feature is None) or (ranges == []):
        return dash.no_update
    else:
        print(ranges)
        dff = pd.read_json(js_df, orient='split')
        df_sliced_min = dff[(dff[feature] < ranges[0])]
        df_sliced_max = dff[(dff[feature] > ranges[1])]

        fig = go.Figure()
        fig.add_trace(go.Scatter3d(x=df_sliced_min['X'], y=df_sliced_min['Y'], z=df_sliced_min['Z'], mode='markers', marker={'color': '#1F7BAA', 'size': 6}))
        fig.add_trace(go.Scatter3d(x=df_sliced_max['X'], y=df_sliced_max['Y'], z=df_sliced_max['Z'], mode='markers', marker={'color': '#FF206E', 'size': 6}))

    return fig

Try this:

@callback(
    Output('graph1', 'figure'),
    Input('dropdown1', 'value'),
    Input('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def group1(b1, b2, js_df, feature):
    triggered_id = ctx.triggered_id
    if triggered_id == 'dropdown1':
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
        return update_with_rangeslider(js_df, feature, b2)

def plot3d(js_df):
    dff = pd.read_json(js_df, orient='split')
    fig = go.Figure()
    fig.add_trace(go.Scatter3d(x=dff['X'], y=dff['Y'], z=dff['Z'], mode='markers', marker={'color': '#F3CAD2', 'size': 4}))
    return fig

def update_with_rangeslider(js_df, feature, ranges):
    if (feature is None) or (ranges == []):
        return dash.no_update
    else:
        print(ranges)
        dff = pd.read_json(js_df, orient='split')
        df_sliced_min = dff[dff[(dff[feature] < ranges[0])]]
        df_sliced_max = dff[dff[(dff[feature] > ranges[1])]]

        fig = go.Figure()
        fig.add_trace(go.Scatter3d(x=df_sliced_min['X'], y=df_sliced_min['Y'], z=df_sliced_min['Z'], mode='markers', marker={'color': '#1F7BAA', 'size': 6}))
        fig.add_trace(go.Scatter3d(x=df_sliced_max['X'], y=df_sliced_max['Y'], z=df_sliced_max['Z'], mode='markers', marker={'color': '#FF206E', 'size': 6}))

    return fig

I got this error, “ValueError: Boolean array expected for the condition, not objects”

The figure was working with the same code when I did not use the callback_context. but I had this problem that the figure was showing after choosing from dropdown1, and dropdown2, and moving the slider.

If possible, can you give some test data and your app.py so I can run this on my machine?

Thanks.

Please check this link. https://github.com/p-bahrami/Dash_plotting_issue.git

Basically, I want to update the figure when moving the slider.

@Peyman,

You are wanting to graph the between values, is this correct?

No, when moving the lower slider, lower values shows on 3dplot in blue
and when moving the upper slider, higher values shows as red.

1 Like

However I dont want to plot that var1 on 3d plot, I want to change the color of the associated Z.

Try this:

import dash
from dash import Dash, html, dcc, Output, Input, State, MATCH, ALL, callback, ctx
import plotly.express as px
import pandas as pd
import numpy as np
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
import json

path = ""

app = Dash(__name__, external_stylesheets=[dbc.themes.SUPERHERO])

app.layout = html.Div([
            dbc.Row([
                dbc.Col([
                    dcc.Dropdown(id='dropdown1',
                                options=['Dataset1'],
                                clearable=True,
                                value=[]
                                )
                ], width=2,
                   style={'margin-left': 90, 'margin-top': 20, 'color': 'black'}
                ),

                dbc.Col([
                    dcc.Dropdown(id='dropdown2',
                                 options=[],
                                 clearable=True,
                                 value=[]
                                 )
                ], width=2,
                    style={'margin-left': 0, 'margin-top': 20, 'color': 'black'}
                ),

                dbc.Col(id='AAA', children=[
                    dcc.RangeSlider(0, 100, value=[], allowCross=False, marks=None,
                                    tooltip={"placement": "top", "always_visible": True},
                                    id='my_rangeslider',
                                    )
                ], width=6, style={'width': 335, 'display': 'none'}),
            ]),

            dbc.Row([
                dbc.Col([
                    html.Hr(
                        style={'margin-top': 30, 'margin-right': -255, 'opacity': 0.7, 'color': 'white'}
                    ),
                ])
            ]),

            dbc.Row([
                dbc.Col([
                    dcc.Graph(id='graph3',
                              figure={},
                              style={'width': 600, 'height': 550, 'margin-left': 90}
                              )
                ], width=6),
            ]),

            dbc.Row([
                dbc.Col([
                    dcc.Store(id='table1')
                ]),
            ])
])


# call back to store and share the df between callbacks
@app.callback(
    Output('table1', 'data'),
    Input('dropdown1', 'value'),
    prevent_initial_call=True
)
def store_df(chosen_dataset):
    if chosen_dataset is None:
        return dash.no_update
    else:
        df = pd.read_csv(path + chosen_dataset + '.csv')
        df_json = df.to_json(date_format='iso', orient='split')
    return df_json


# it gets dataset from dropdown1 and returns the feature options in dropdown2
@app.callback(
    Output('dropdown2', 'options'),
    Input('table1', 'data'),
    prevent_initial_call=True
)
def set_feature_options(js_df):
    if js_df is None:
        return dash.no_update
    else:
        dff = pd.read_json(js_df, orient='split')
        features = np.sort(dff.columns.values[1:2])
        return features


@app.callback(
    Output('my_rangeslider', 'min'),
    Output('my_rangeslider', 'max'),
    Output('my_rangeslider', 'marks'),
    Output('my_rangeslider', 'value'),
    Output('AAA', 'style'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def set_rangeslider_values(js_df, feature):
    if (feature == []) or (feature is None):
        return dash.no_update
    else:
        R = {'display': 'block'}
        dff = pd.read_json(js_df, orient='split')
        minn = round(dff[feature].min(), 2)
        maxx = round(dff[feature].max(), 2)
        min_value = dff[feature].min()
        max_value = dff[feature].max()
        v25 = dff[feature].quantile(0.25)
        v75 = dff[feature].quantile(0.75)
        lower_inner_fence = v25 - 1.5 * (v75 - v25)
        upper_inner_fence = v75 + 1.5 * (v75 - v25)

        marks = {int(lower_inner_fence): {'label': 'Lower Limit', 'style': {'position': 'absolute'}},
                 int(v25): '%25', int(v75): '%75',
                 int(upper_inner_fence): 'Upper Limit'}
        return minn, maxx, marks, [min_value, max_value], R


@app.callback(
    Output('graph3', 'figure'),
    Input('dropdown1', 'value'),
    Input('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def group1(b1, b2, js_df, feature):
    triggered_id = ctx.triggered_id
    if triggered_id in ['dropdown1', 'dropdown2', 'table1']:
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
        fig = update_with_rangeslider(js_df, feature, b2)
        return fig

def plot3d(js_df):
    dff = pd.read_json(js_df, orient='split')
    fig = go.Figure()

    fig.add_trace(go.Scatter3d(x=dff['EW'], y=dff['NS'], z=dff['Z'], mode='markers', marker={'color': '#F3CAD2', 'size': 4}))
    fig.update_layout(hovermode='closest', margin={'l': 0, 'r': 0, 't': 30, 'b': 0}, showlegend=False, template="plotly_white"),
    fig.update_scenes(zaxis_autorange="reversed")
    return fig

def update_with_rangeslider(js_df, feature, ranges):
    if (feature is None) or (ranges == []):
        return dash.no_update
    else:
        print(ranges)
        dff = pd.read_json(js_df, orient='split')
        df_sliced_min = dff[(dff[feature] < ranges[0])]
        df_sliced_max = dff[(dff[feature] > ranges[1])]
        df_sliced_between = dff[(dff[feature] >= ranges[0]) & (dff[feature]<= ranges[1])]
        fig = go.Figure()
        fig.add_trace(go.Scatter3d(x=df_sliced_min['EW'], y=df_sliced_min['NS'], z=df_sliced_min['Z'], mode='markers', marker={'color': '#1F7BAA', 'size': 6}))
        fig.add_trace(go.Scatter3d(x=df_sliced_max['EW'], y=df_sliced_max['NS'], z=df_sliced_max['Z'], mode='markers', marker={'color': '#FF206E', 'size': 6}))
        fig.add_trace(go.Scatter3d(x=df_sliced_between['EW'], y=df_sliced_between['NS'], z=df_sliced_between['Z'], mode='markers',
                                   marker={'color': 'green', 'size': 6}))

        return fig



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

The issue seemed to be with the inputs above for some reason.

2 Likes

Thank you very much. It worked. may I ask what was my problem here? you did changes on this callback and on
if triggered_id in [‘dropdown1’, ‘dropdown2’, ‘table1’]:

def group1(b1, b2, js_df, feature):
    triggered_id = ctx.triggered_id
    triggered_id = ctx.triggered_id
    if triggered_id == 'dropdown1':
    if triggered_id in ['dropdown1', 'dropdown2', 'table1']:
        return plot3d(js_df)
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
    elif triggered_id == 'my_rangeslider':
        return update_with_rangeslider(js_df, feature, b2)
        fig = update_with_rangeslider(js_df, feature, b2)
        return fig

I’m assuming that because there was no valid response, it was actually breaking the callback flow.

1 Like

But, the benefit is that if you were to load a new file, it would reset. :wink:

I tested this way just now, and it still worked:

@app.callback(
    Output('graph3', 'figure'),
    Input('dropdown1', 'value'),
    Input('my_rangeslider', 'value'),
    Input('table1', 'data'),
    Input('dropdown2', 'value'),
    prevent_initial_call=True
)
def group1(b1, b2, js_df, feature):
    triggered_id = ctx.triggered_id
    if triggered_id in ['dropdown2']:
        return plot3d(js_df)
    elif triggered_id == 'my_rangeslider':
        fig = update_with_rangeslider(js_df, feature, b2)
        return fig
    return dash.no_update

This new code does not bring the initial plot after choosing the dataset from dropdown1. the plot shows after choosing from the second dropdown. actually, with the previous code, I want to keep the position of the plot fixed. Every time I move the slider, the plot resets.

On the layout, you need to pass uirevision=True

As far as not graphing after dataset1 is selected, you technically dont have Var1 to drive the feature. But, you can go to the initial one where it should be graphing it.