✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚡️ Concerned about the grid? Kyle Baranko teaches how to predicting peak loads using XGBoost. Register for the August webinar!

"Click" Function in Button Config Not Defined

Hi,

I’m running into an issue where clicking a button for the second time throws a JavaScript error.
I’m trying to make a function that takes the children of a row and a figure in storage, appends that figure to the children, and returns the appended children.
Right now I have this callback function:

@app.callback(
    Output('row-container', 'children'),

    [Input('append-button', 'n_clicks'),
     Input('fig-store', 'data')],
    [State('row-container', 'children')]
)
def update_row(n_clicks, stored_fig, current_figs):
    """ Duplicates the contents of a row and appends that to itself

    Parameters
    -------------
    n_clicks : Int, None
        The number of times the button is pressed
    stored_fig : go.Figure, None
        The first figure in the row (in local storage)
    current_figs : List, None
        The children of the row that is currently displayed on the page

    Returns
    ---------
    new_children : List
        current_figs with stored_fig appended to it
    """

    # If the figures are currently on the screen:
    if current_figs:
        new_children = current_figs
    else:
        new_children = []

    # If the button was pressed and the stored figure exists
    if n_clicks and stored_fig:
        new_children.append(stored_fig)
    elif stored_fig:  # The stored_fig changed
        new_children = [stored_fig]

    return new_children

This works just fine the first time I press that append-button and it appends the stored_fig to the children. However, the second time I press the button I get this error:

From looking deeper with ipdb, it looks like new_children is properly updated and returned but somewhere deep in Dash a bad response is thrown. Here’s what Chrome’s console has to say about it:

Uncaught Error: must provide button 'click' function in button config
    at l.c.createButton (plotly-1.49.4.min.js:7)
    at plotly-1.49.4.min.js:7
    at Array.forEach (<anonymous>)
    at plotly-1.49.4.min.js:7
    at Array.forEach (<anonymous>)
    at l.c.updateButtons (plotly-1.49.4.min.js:7)
    at l.c.update (plotly-1.49.4.min.js:7)
    at new l (plotly-1.49.4.min.js:7)
    at e.exports (plotly-1.49.4.min.js:7)
    at Object.e.exports [as manage] (plotly-1.49.4.min.js:7)

That l.c.createButton line is:

Do y’all know what’s going on here? I’m not sure why the click function isn’t defined for a button, why the button works the first time, or why createButton is even being called.

1 Like

Hi @Zach welcome to the forum! One thing which crosses my mind is that with your function, the second time you click the button you will probably have duplicated ids in your layout (it’s not clear to me what you store in the store component, is it a figure dict or a full dcc.Graph object?? If you defined its id it will appear twice in the layout which should cause problems). Could this be the source of the problem?

Hi @Emmanuelle!

Hmm, it doesn’t look like that was the issue (though it may have become one down the line). I added in this before the return new_children line:

for i in range(len(new_figs)):
        child = new_figs[i]
        child_id = child['props']['children']['props']['id']
        if not child_id.endswith(f"-{i}"):
            child['props']['children']['props']['id'] += f"-{i}"

and did set everything to a unique ID (e.g. figure-0 instead of figure), but it still gives the “click” error.

The row-container and fig-store data come in as dicts, but were originally dbc.Col objects with dcc.Graphs in them.

More context in case it helps:
The app.layout lays out the section for the figures as:

dcc.Loading(id='figure-loader'
    dbc.Row(id='figure-container'
        [
            dbc.Row(
                dbc.Col(dcc.Graph(id='figure-0', figure=fig))
            ),
            dbc.Row(
                dbc.Col(dcc.Graph(id='figure-1', figure=fig_copy_1))
            ),
            ...
            dbc.Row(
                dbc.Col(dcc.Graph(id='figure-n', figure=fig_copy_n)
            )
        ]
    )
)

I’m not sure if each and every row and column needs its own ID, would this help?

I’m working with @Zach to try and figure this out. I believe we have a hacky workaround if we have to resort to it. Though, this doesn’t seem like we’re doing anything particularly odd here so we’ll try to get some minimal sample code that reproduces the problem.

Aha, we found the issue!

Turns out it’s an odd combination of using dcc.Loading and giving the dcc.Graph object modBarButtons. Here’s a small-ish example of the issue:

import copy

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go

# Configure app
app = dash.Dash(
    __name__,
    meta_tags=[{'name': 'viewport', 'content': 'width=device_width'}]
)
app.config.suppress_callback_exceptions = True


# Make a placeholder for the original figure
DUMMY_FIGURE = dcc.Graph(
    id='figure-0',
    figure=go.Figure({
        "data": [{"type": "scatter",
                  "x": [1, 2, 3],
                  "y": [1, 3, 2]}],
        "layout": {"title": {"text": "A Scatter graph"}}
    }),
    ### This config + dcc.Loading = click error ###
    config=dict(
        displayModeBar=True,
        modeBarButtons=[['resetScale2d']]
    )
)


# Add a store for the original figure
figure_store = dcc.Store(id='figure-store')


app.layout = html.Div(
    [
        # Placeholder for the dummy figure store
        html.Div(figure_store, id='figure-store-container', hidden=True),

        # Container for the start button
        html.Div(
            html.Button(
                "Start App",
                id='start-button',
            ),
            id='start-button-container'
        ),

        # Placeholder for the figures
        ### If you make dcc.Loading into html.Div, it works with the Graph config ### noqa
        dcc.Loading(
            id='figure-container'
        ),


        # Placeholder for the duplicate button (hidden at first)
        html.Div(
            html.Button(
                "Duplicate",
                id='duplicate-button',
            ),
            hidden=True,
            id='duplicate-button-container'
        )
    ]
)


@app.callback(
    [Output('duplicate-button-container', 'hidden'),
     Output('figure-store', 'data')],

    [Input('start-button', 'n_clicks')]
)
def show_divs(start_clicks):

    # If you clicked the button, unhide duplicate
    # and make the dummy figure
    if start_clicks and start_clicks > 0:
        state = False
        fig = DUMMY_FIGURE
    else:
        state = True
        fig = None
    return state, fig


@app.callback(
    Output('figure-container', 'children'),

    [Input('duplicate-button', 'n_clicks'),
     Input('figure-store', 'data')],

    [State('figure-store', 'data'),
     State('figure-container', 'children')]
)
def duplicate_fig(duplicate_clicks, new_fig, stored_fig, existing_figs):
    # If there are figures already on the page, grab them
    if existing_figs:
        new_children = existing_figs
    else:
        new_children = []

    # You clicked the duplicate button
    if duplicate_clicks:
        if stored_fig:
            new_children.append(copy.deepcopy(stored_fig))

    # The dummy figure was added to the screen
    elif new_fig:
        new_children.append(new_fig)

    # Set the IDs to be unique
    for i in range(len(new_children)):
        child = new_children[i]
        child_id = child['props']['id']
        if not child_id.endswith(f"-{i}"):
            child['props']['id'] += f"-{i}"

    return new_children


if __name__ == "__main__":
    app.run_server(
        debug=True,
        dev_tools_silence_routes_logging=True
    )

If you either remove

config=dict(
        displayModeBar=True,
        modeBarButtons=[['resetScale2d']]
    )

from the dcc.Graph object, or replace dcc.Loading with an html.Div container, it works.

We’re not exactly sure why it breaks, though, so we’ll be posting it to the Github bug tracker later tonight.

Try this, I’m not sure will it work or not.