How to properly use shapes in dynamic callbacks?

Hello,
I have a dataframe of 4 columns. I have the below code, which uses dynamic callbacks to add/remove charts. col1 and col2 are numeric, col3 is category, and col4 is also numeric.
In my plot, I want to have col4 on the y-axis, col1 and col2 on the x-axis, and col3 as the highlighted background.
I have added the highlighted background using fig.add_shape. If you run my code, it should work well. But I have two issues.

Before I select col1 or col2 from the dropdown menu, the highlighted backgrounds are placed on the chart. I want to trigger them, and show them on the chart after parameter selection.
To highlight the plot, I had used fig.add_vrect or fig.add_hrect, but I couldn’t override the lines on the highlighted sections and the color of the lines looked bad, so I start using fig.add_shape.

Also, how I can fix colors for the lines? Right now, the first selected parameter from the dropdown menu is going to be blue and the second parameter is red. I want to have my col1 always in one defined color, and col2 in another fixed color.

Thanks in advance.

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

df={'col1':[12,15,25,33,26,33,39,17,28,25],
    'col2':[35,33,37,36,36,26,31,21,15,29],
    'col3':['A','A','A','A','B','B','B','B','B','B'],
    'col4':[1,2,3,4,5,6,7,8,9,10]
    }
df = pd.DataFrame(df)

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

app.layout = html.Div([
    html.Div(children=[
        html.Button('add Chart', id='add-chart', n_clicks=0)
    ]),
    html.Div(id='container', children=[])
])

@app.callback(
    Output('container', 'children'),
    [Input('add-chart', 'n_clicks'),
    Input({'type': 'remove-btn', 'index': ALL}, 'n_clicks')],
    [State('container', 'children')],
    prevent_initial_call=True
)
def display_graphs(n_clicks, n, div_children):

    ctx = dash.callback_context
    triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]

    elm_in_div = len(div_children)

    if triggered_id == 'add-chart':
        new_child = html.Div(
            id={'type': 'div-num', 'index': elm_in_div},
            style={'width': '25%',
                   'display': 'inline-block',
                   'outline': 'none',
                   'padding': 5},
            children=[
                dcc.Dropdown(id={'type': 'feature-choice',
                                 'index': n_clicks},
                             options=df.columns.values[0:2],
                             multi=True,
                             clearable=True,
                             value=[]
                ),
                dcc.Graph(id={'type': 'dynamic-graph',
                              'index': n_clicks},
                          figure={}
                ),
                html.Button("Remove", id={'type': 'remove-btn', 'index': elm_in_div})
            ]
        )
        div_children.append(new_child)
        return div_children

    if triggered_id != 'add-chart':
        for idx, val in enumerate(n):
            if val is not None:
                del div_children[idx]
                return div_children

@app.callback(
    Output({'type': 'dynamic-graph', 'index': MATCH}, 'figure'),
    [Input({'type': 'feature-choice', 'index': MATCH}, 'value')]
)
def update_graph(X):
    if X is None:
        raise PreventUpdate

    Xmin = df[X].min().min()
    Xmax = df[X].max().max()

    # to find the height of y-axis(col4)
    col4_max = df['col4'].max()
    col4_min = df['col4'].min()

    fig1 = px.line(df, x=X, y='col4')
    fig1.update_layout({'height': 600,
                'legend': {'title': '', 'x': 0, 'y': 1.06, 'orientation': 'h'},
                'margin': {'l': 0, 'r': 20, 't': 50, 'b': 0},
                'paper_bgcolor': 'black',
                'plot_bgcolor': 'white',
                }
    )

    fig1.update_yaxes(range=[col4_max, col4_min], showgrid=False)
    fig1.update_xaxes(showgrid=False)

    categ_col3 = df.col3.dropna().unique()
    colors = ['#54FF9F', '#87CEFF']

    for (i,j) in zip(categ_col3, colors):

        index_min = df.loc[df.col3 == i].index[0]
        index_max = df.loc[df.col3 == i].index[-1]
        if index_min == 0:
            cat_min = df['col4'][index_min]
        else:
            cat_min = df['col4'][index_min-1]
        cat_max = df['col4'][index_max]

        fig1.add_shape(type="rect", x0=Xmin, y0=cat_min, x1=Xmax, y1=cat_max,
                       fillcolor=j, layer='below', opacity=0.5,
                       )
    return fig1

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

This happens, because your second callback gets fired ad startup. If you include prevent_initial_call=True, the figure of the graph remains empty.

If this should be hard coded (column ↔ color), you could do something like this:

    lookup = {f'col{i}': c for i, c in zip(range(1, 5), ['yellow', 'orange', 'red', 'pink'])}
    color_map = [lookup[item] for item in X]

    fig1 = px.line(df, x=X, y='col4', color_discrete_sequence=color_map)

An additional comment:

you could

from dash import ctx

and later

triggered_id = ctx.triggered_id

instead of

ctx = dash.callback_context
triggered_id = ctx[0]['prop_id'].split('.')[0]

Hello @AIMPED . Thank you very much for your reply. your solution solved the first issue. Also, for the second issue, it works perfectly on the given dataframe that I brought as an example. However, in my case, I have a lot of columns, and I think it is not good to assign a color to each column. I am looking for a way to assign 3 or 4 sharp colors and only use those colors. I won’t have more than 3 or 4 parameters on the chart. Thank you again

What do you mean by 3 or 4 colors?

I thought you wanted to assign a certain color to certain colum names, so that for example ‘col1’ always is yellow, independent of the order it has been chosen from the dropdown.

So basically, you want to choose the first 4 colors of the colormap?

I want to have the 1st, 2nd, 3rd, and 4th selected columns as red, blue, black, and yellow.
but the 1st selected column is not necessarily col1. it might be any columns. so, for example, if I choose col1, col2, or col 20 (in my case I have around 30 columns with different headers) for my first pick from dropdown menu, it is going to be red.

Hi,

like this?

fig1 = px.line(df, x=X, y='col4', color_discrete_sequence=['red', 'blue', 'black', 'yellow'])

Keep in mind that if you choose more than 4 columns, the 5. line will be red again.

Thank you.
I tried this. this one raises the same error I initially mentioned. for example, if you add col1 for the first pick, it is gonna be red. then, if you pick col2 as the second pick, it is gonna be blue. Now, if we deselect col1, the col2 switches to red, and it is not going to stay blue.

Hi @Peyman ,

finally I understood what you are aiming for. To find a solution for this one took me a while! I created a stripped down version of your code, the basic functionality is given though.

trace_colors

Thank you very much, and I am really appreciated for the time you spent on this. It probably takes a week for me to digest it. :smiley:

@Peyman you are welcome. Let me help you to digest it:

In order to achieve this behaviour you need to know, which traces (plotted columns) are in the current figure. So the callback needs the State of the current figure.

Then you compare the traces in the current figure with the current dropdown selection and decide, which traces to:

  • delete (because they are not in the current selection but in the current figure)
  • keep (because they are in the current selection and in the current figure)
  • create (because they are in the current selection but not in the figure)

From the traces you have to keep you extract the used colors. By this you know which colors are available for the new traces. In the end you put the traces you kept together with the traces you created into the new figure and update the dcc.Graph

You actually do not need the class I created, but I had something like this in my mind for a time and maybe I will continue working on this.

1 Like