Search code examples
pythonplotlyjupyterinteractivelinegraph

Removing data from a line graph interactively in Jupiter notebook


I have a NumPy array which contains data from several samples. Some of the samples are outliers and need to be removed via visual inspection. Is there a way to make an interactive line plot in a jupyter notebook where a user can select a line on the plot by clicking it and have that line disappear/be highlighted and the data be marked for removal?

So far the best I have come up with is using Plotly:

import plotly.graph_objects as go

x = np.linspace(0,100)
y = np.random.randint(5, size=(5, 100))

fig = go.Figure()

for line in range(5):
    fig.add_trace(go.Line(x=x, y=y[:,line],mode='lines'))

f = go.FigureWidget(fig)

f

Plotly output line graph

Using this code I can get a line graph with lines that are selectable by selecting the corresponding label in the figure legend, but this quickly becomes unfeasible with more samples. Is there a way to do this without plotting a legend and having the lines be selectable directly in the graph?

Thanks


Solution

  • You can use click events which allow you to define a callback that is bound to each trace. Here is an example of a callback function called update_trace that will remove a trace when it's clicked on (the @out.capture decorator isn't necessary, but can be useful for debugging using print statements):

    import numpy as np
    import plotly.graph_objects as go
    from ipywidgets import Output, VBox
    
    np.random.seed(42)
    x = np.linspace(0,100)
    y = np.random.randint(5, size=(5, 50))
    
    fig = go.Figure()
    
    for line in range(5):
        fig.add_trace(go.Scatter(x=x, y=y[line,:],mode='lines',visible=True,name=f'trace_{line+1}'))
    
    f = go.FigureWidget(fig)
    
    out = Output()
    
    @out.capture(clear_output=False)  
    def update_trace(trace, points, selector):
        
        ## determine whether trace was clicked on
        if points.point_inds == []:
            pass
        else:
            selected_trace_name = trace.name
            for f_trace in f.data:
                if (selected_trace_name == f_trace.name) & (f_trace.visible == True):
                    f_trace.visible = False
                    print(f"removing {selected_trace_name}")
    
    traces = f.data
    for trace in traces:
        trace.on_click(update_trace)
    
    VBox([f, out])
    

    enter image description here