Plotly update figure on selection

Hello,

I am trying to update figure on selection but it doesn’t turn back when when clicked “reset” or double-click


app = Dash(
    suppress_callback_exceptions=True
)

def getGraph(x,y,selectedData):
    selectedPoints,unselectedPoints = getPoints(x,y,selectedData)
    fig = go.Figure()
    fig.add_trace(
        go.Scatter(
            x = selectedPoints[:,0],
            y = selectedPoints[:,1],
            mode="markers",
            marker=dict(color="crimson"),
            name="selectedScatter"
        )
    )
    fig.add_trace(
        go.Scatter(
            x = unselectedPoints[:,0],
            y = unselectedPoints[:,1],
            mode="markers",
            name="unselectedScatter",
            marker=dict(color="cyan")
        )
    )
    return fig



num_size = 1000
x = np.random.random_integers(low=0,high=100,size=num_size)
y = np.random.random_integers(low=0,high=100,size=num_size)
originalFig = go.Figure()
originalFig.add_trace(
    go.Scatter(
        x=x,
        y=y,
        mode="markers",
        marker=dict(color="cyan")
    )
)

app.layout = html.Div(
    [
        html.Div(
            dcc.Graph(
                id="the-graph",
                style={
                    "width":"90vw"
                }
            ),
            style= {
                "display":"flex",
                "align-items":"center",
                "justify-content":"center"
            }
        ),
    ]
)

@callback(
    Output("the-graph","figure"),
    Input("the-graph","selectedData")
)
def updateFigure(selectedData):
    if selectedData:
        return getGraph(x,y,selectedData)
    else:
        return originalFig
    

app.run_server()

I observed that when selected using “box-select” or “lassoSelect” then selectedData dict is generated populating it from null to dict but after the figure is updated the selectedData turns back to empty list and not null, it becomes

selectedData = {“points”:}

I would like it to revert back to originalFig when double-click event, but I haven’t found anything related to that.

is there a way to rectify this?

I don’t have a full solution to this, only a couple of thoughts that might hopefully be useful.

An easy way of reverting back to the original figure could be to just add a ‘reset’ button to the layout and use a callback handling its click data - would that work for what you want?

If not, you have a few lines in your code that effectively prevent the figure ever returning to its original setting.

    if selectedData["points"] != []:
        selected_points = selectedData["points"]
    else:
        selected_points = previousSelectedPoints

I can see why you have these - your callback replaces the figure, and that resets the selection to empty and you need to prevent the callback retriggering and immediately resetting the figure to the original. But I can’t see how to change it to get the behaviour you need if you’re replacing the figure.

Maybe an alternative would be to add a new trace to the figure (using Patch) showing the selected points, leaving the original trace unchanged, and have your callback manipulate only this second trace? I don’t really know if that would work - it depends on just what events get triggered when the second trace is modified - but it’s what I’d try if no-one else comes up with a better suggestion.

As a separate thing, using a global to store previous state will go wrong if there is more than one user - see the link below. The usual solution is to use a dcc.Store() to store state, not a global.

1 Like

I really appreciate the response and I found a temporary workaround just an hour ago, though I will keep this open until someone from the team confirm if theres an easy way to do It.

I am assuming you understood my problem, Isn’t it a simple thing like shouldn’t updating a figure on selection a simple thing in concept but when I found the things that change on clicks on graph are so many

this is the work-around that works perfectly specifically for me:


app = Dash(
    suppress_callback_exceptions=True
)

def getGraph(x,y,selectedData):
    selectedPoints,unselectedPoints = getPoints(x,y,selectedData) #get the seleted points  and unselected points in least time complexity
    fig = go.Figure()
    fig.add_trace(
        go.Scatter(
            x = selectedPoints[:,0],
            y = selectedPoints[:,1],
            mode="markers",
            marker=dict(color="crimson"),
            name="selectedScatter"
        )
    )
    fig.add_trace(
        go.Scatter(
            x = unselectedPoints[:,0],
            y = unselectedPoints[:,1],
            mode="markers",
            name="unselectedScatter",
            marker=dict(color="cyan")
        )
    )
    return fig



num_size = 100
x = np.random.random_integers(low=0,high=100,size=num_size)
y = np.random.random_integers(low=0,high=100,size=num_size)
originalFig = go.Figure()
originalFig.add_trace(
    go.Scatter(
        x=x,
        y=y,
        mode="markers",
        marker=dict(color="cyan"),
        name="totalScatter",
        showlegend=True
    )
)

app.layout = html.Div(
    [
        html.Div(
            dcc.Graph(
                id="the-graph",
                style={
                    "width":"90vw"
                }
            ),
            style= {
                "display":"flex",
                "align-items":"center",
                "justify-content":"center"
            }
        ),
    ]
)

@callback(
    Output("the-graph","figure"),
    [
        Input("the-graph","selectedData"),
        Input("the-graph","relayoutData")
    ],
    prevent_initial_call=True
)
def updateFigure(selectedData,relayoutData,*args,**kwargs):
    if (selectedData is not None) and ((relayoutData.get("xaxis.showspikes",True) == True) or (relayoutData.get("yaxis.showspikes",True) == True)): # condition to trigger when there is some selected data but also the "reset" button is not clicked ( when reset is clicked it actually becomes False)
        fig =  getGraph(x,y,selectedData)
        if (relayoutData.get("xaxis.range[0]",None) is not None): # this is when you zoom out also extracting the ranges i.e., the zoom limiits this is added because when you zoom it reverted mine to original image. 
            fig.update_xaxes(range=[relayoutData.get("xaxis.range[0]"),relayoutData.get("xaxis.range[1]")])
            fig.update_yaxes(range=[relayoutData.get("yaxis.range[0]"),relayoutData.get("yaxis.range[1]")])
        return fig        
    else: # rest any case ( by pressing reset button or refresh the page) )just display the normal plot
        originalFig.update_layout(**relayoutData)
        return originalFig

app.run_server()

I would actually like to have a simpler solution that this if at all its possible, The only things that throws wrench in my code is the selection mechanism where when you selected data using “select” or “lassoSelect” the box just highlights it so it stores points in selectedData but as soon as fig is updated the points becom null again (selectedData = {“points”:})

I’ve come across this too. The problem is, that the selected data is not reset automatically. I usually just add the selected as output when using it as input to trigger the callback. As return value for the selected data I use an empty dict, None, or empty list. I can’t remember exactly right now.

1 Like