Hot-key behaviour to remove traces from plot via legend

I’m working on a functionality which adds traces to a plot based on user preferences , since traces need to be removed as well, i need to come up a easy way to remove traces from plot. My code (given below) implements this by using dash-extensions EventListerner and restyleData attribute of dcc.graph.

When control key is pressed & a trace in legend is clicked, it removes a trace. The problem that im currently facing is that, it doesnt work reliably - sometimes it removes the trace, some times it doesn’t - just disables it.

import dash
from dash_extensions.enrich import dcc
from dash_extensions.enrich import html
from dash.dependencies import Input, Output
from dash_extensions.enrich import DashProxy, html, Input, Output, State
from dash_extensions import EventListener
from dash.exceptions import PreventUpdate
import plotly.graph_objects as go
event = {"event": "keydown", "props": ['ctrlKey']}

kwargs = {
    "meta_tags": [{"name":"viewport", "content":"width=device-width, initial-scale=1, shrink-to-fit=no", "charset": "utf-8"}],
    "title" : "Event Listener",
}

app =  DashProxy(__name__, **kwargs)
def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1,2,3], name=f'Trace 1'))
    fig.add_trace(go.Scatter(x=[4, 5, 6], y=[1,2,3], name=f'Trace 2'))
    fig.add_trace(go.Scatter(x=[7, 8, 9], y=[1,2,3], name=f'Trace 3'))
    return fig
app.layout = html.Div(
    [
        EventListener(
        events=[event], logging=True, id="el",
    ),
    dcc.Graph(
            id="graph",
            figure=dummy_graph()
        ),
        
        html.Div(id="output"),
        html.Div(id="log")
    ]
)


@app.callback(
    [Output("output","children"),
    Output("el","event"),
    Output("graph","figure")],
    [Input("graph", "restyleData"),
     Input("el", "event"),
     Input("graph", "figure")],
    prevent_initial_call=True
)
def display_relayout_data(relayout_data, e,figure):
    triggered_input = dash.callback_context.triggered[0]["prop_id"].split(".")[0]
    if triggered_input == "graph":
       # return str(triggered_input)
        if e is not None:
            if e['ctrlKey'] == True:
                figure['data'].pop(relayout_data[1][0])
                return "", None, figure
    else:
        return "", None, figure
    return "", None, figure

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

You may notice that im returning event attribute as None in callback, that is because i noticed that it needs to be cleared every time im done with it otherwise it retains its state and a trace is deleted even if cntrl key is not pressed.

I need to some input on this implementation - is there a cleaner and more reliable way to achieve what im doing ?

As show below in video below - im holding down cntrl key while also clicking traces - it doesn’t work smoothly and seems buggy.

legend_click

Hello @ptser,

Welcome to the community!

Yes, it does seem buggy.

The key down listener isnt always accurate, sometimes it doesnt register the keydown properly, it also doesnt clear the ctrlKey upon keyup always. For example, press ctrlKey and then release and click, it will still act like it is triggered.

Here is your example working with the new Patch():

from dash import ctx, Patch, no_update
from dash_extensions.enrich import dcc
from dash_extensions.enrich import DashProxy, html, Input, Output, State
from dash_extensions import EventListener
from dash.exceptions import PreventUpdate
import plotly.graph_objects as go

event = {"event": "keydown", "props": ['ctrlKey']}

kwargs = {
    "meta_tags": [
        {"name": "viewport", "content": "width=device-width, initial-scale=1, shrink-to-fit=no", "charset": "utf-8"}],
    "title": "Event Listener",
}

app = DashProxy(__name__, **kwargs)


def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], name=f'Trace 1'))
    fig.add_trace(go.Scatter(x=[4, 5, 6], y=[1, 2, 3], name=f'Trace 2'))
    fig.add_trace(go.Scatter(x=[7, 8, 9], y=[1, 2, 3], name=f'Trace 3'))
    return fig


app.layout = html.Div(
    [
        EventListener(
            events=[event], logging=True, id="el",
        ),
        dcc.Graph(
            id="graph",
            figure=dummy_graph()
        ),

        html.Div(id="output"),
        html.Div(id="log")
    ]
)


@app.callback(
    [Output("output", "children"),
     Output("graph", "figure")],
    [Input("graph", "restyleData"),
     State("el", "event")],
    prevent_initial_call=True
)
def display_relayout_data(relayout_data, e):
    triggered_input = ctx.triggered_id
    if triggered_input == "graph":
        print(e)
        if e is not None:
            if e['ctrlKey'] == True:
                new_fig = Patch()
                del new_fig['data'][relayout_data[1][0]]
                return "", new_fig
    raise PreventUpdate


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

I think the most failproof way would be to add a listener to the legend via a JS script.

Thank you for your input.
It seems this solution is buggy as well - try pressing and then releasing cntrl key without clicking on legend and then click a trace on legend - it will remove the trace, not only that - it will also remove the other traces if you keep on clicking the traces in legend.

How can I interface JS script to this ? I have no idea about Js - just nudge me in the right direction please.

Here, try this instead:

from dash import ctx, Patch, no_update
from dash_extensions.enrich import dcc
from dash_extensions.enrich import DashProxy, html, Input, Output, State
from dash_extensions import EventListener
from dash.exceptions import PreventUpdate
import plotly.graph_objects as go

event = {"event": "click", "props": ['ctrlKey']}

kwargs = {
    "meta_tags": [
        {"name": "viewport", "content": "width=device-width, initial-scale=1, shrink-to-fit=no", "charset": "utf-8"}],
    "title": "Event Listener",
}

app = DashProxy(__name__, **kwargs)


def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], name=f'Trace 1'))
    fig.add_trace(go.Scatter(x=[4, 5, 6], y=[1, 2, 3], name=f'Trace 2'))
    fig.add_trace(go.Scatter(x=[7, 8, 9], y=[1, 2, 3], name=f'Trace 3'))
    return fig


app.layout = html.Div(
    [
        EventListener(
            events=[event], logging=True, id="el",
        ),
        dcc.Graph(
            id="graph",
            figure=dummy_graph()
        ),

        html.Div(id="output"),
        html.Div(id="log")
    ]
)


@app.callback(
    [Output("output", "children"),
     Output("graph", "figure")],
    [Input("graph", "restyleData"),
     State("el", "event")],
    prevent_initial_call=True
)
def display_relayout_data(relayout_data, e):
    triggered_input = ctx.triggered_id
    if triggered_input == "graph":
        print(e)
        if e is not None:
            if e['ctrlKey'] == True:
                new_fig = Patch()
                del new_fig['data'][relayout_data[1][0]]
                return "", new_fig
    raise PreventUpdate


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

Click events also have ctrlKey. :wink:

1 Like

This works much better.
But something weird is happening , when i delete a trace, say trace 1 while holding ctrl down clicking, now for trace 2, dont release the ctrl bttn and just click on it, it’ll just disable the trace in graph and not actually remove it (the callback is not triggered ) . It is not until the second click while holding down ctrl, the trace is removed.

Video demonstrates the issue:

legend_click_patch

why do you this is? Im unable to resolve it

Strange, I thought I tried doing that.

The graph might be getting out of sync somehow between the JS and server.

any ideas on how to resolve this issue ?

I tried disabling click behavior of legend but that doesn’t trigger the cb on click (restyleData attribute is None).
I think there might be some collision (idk what to call it) b/w our callback and default click behavior of legend.

One other thing that i just noticed - if you delete trace 1 and then delete trace 3 , it deletes it properly (and vice versa). If you delete trace 1 and then go on to delete trace 2 - it just disables it (have to click it again to remove it).

The following video demonstrates what i just said:

legend_click_patch_2

Ok, maybe that’s how I tested it.

I think it has something to do with how the relayout is working. I think when you remove the first data set, it acts funky.

Check this out:

When i remove traces from bottom to top, it works perfectly but when i remove traces from top to bottom, it does the double click thingy to remove trace on every other trace. See video below:

legend_click_patch_3

Yes, something seems to be not allowing the restyle to trigger when removing the first set in the data.

Very strange indeed.

do you have any solution in mind ? Im hitting brick wall with this