Black Lives Matter. Please consider donating to Black Girls Code today.

Removing some traces from scatter plot

Hi,

I’m working on interactive plots in Jupyter Notebook. I have a scatter plot figure where traces may be added that I want to remove then. I would like to do so by clicking on them.

I am trying to use the on_click method : when a trace is added to the figure fig, a click_remove callback is consequently added for this trace by using fig.data[-1].on_click(click_remove). Inside click_remove the fig.data tuple is updated, and the “can only assign to data a permutation of a subset of data” condition is verified.

However, it ends up with IndexError or KeyError. It seems that it is because all traces, even those not clicked, are “refreshed” (or something like that), and at some point the indexation is broken because a trace was removed.

I have found a workaround by setting visible=False for wanted traces, but it’s not really satisfying. I also tried using clickmode='select' and fig.data[-1]._select_callbacks.append(click_remove) but the callback seems to never be called then.

Below is a simple Jupyter Notebook code to replicate the behavior (first post here, let me know if there is a better way to provide code). For this exemple I got following errors:

  • IndexError: tuple index out of range, if clicking on trace 0 to 3.
  • KeyError: 4, if clicking on trace 4 (the last added).

Code:

import numpy as np
import plotly.graph_objects as go
from IPython.display import display

def click_remove(trace, points, state):
    
    print(points.trace_name)
    
    # skip if clicking on another trace
    # because when clicking on a trace 
    # all traces are activating this callback
    if not points.point_inds:
        print('  Skip.')
        return
    print('  Remove.')
    
    idx = points.trace_index
    new_data = list(fig.data)
    new_data.pop(idx)

    # can only assign to data a permutation of a subset of data
    assert np.all(
        np.array([id(t) for i, t in enumerate(fig.data)
                  if not i == idx ])
        == np.array([id(t) for t in new_data])
    )

    fig.data = new_data
    
    # Workaround by setting `visible=False`
    # fig_spectrum.data[points.trace_index].visible = False

nlines, npoints = 5, 100
lines = [np.random.randn(npoints)+5*i for i in range(nlines)]

fig = go.FigureWidget(layout=dict(hovermode='closest'))

for line in lines:
    fig.add_trace(go.Scatter(y=line))
    
    # What I would like to do
    fig.data[-1].on_click(click_remove)
    
    # I also tried using clickmode='select' and following line:
    #fig.data[-1]._select_callbacks.append(click_remove)
    
# Even setting the callback on only one trace will throw same errors
#fig.data[2].on_click(click_remove)
    
display(fig)

@aimep
Try setting fig.data[idx].visible=False instead of

new_data = list(fig.data)
new_data.pop(idx)

Yes, it is working if doing so (already said in the original post, and commented line in the code), but I wanted to know if there is a better way.

OK!!! I read only your code and a few lines of text :slight_smile:

Actually, the lines

new_data = list(fig.data)
new_data.pop(idx)
fig.data = new_data

are working, if not called inside the on_click callback.

Actually those lines are also working inside the callback, but the problem is that once the callback ends, something more happens, that is throwing the errors. Do you know what ?

Below is the error:

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/ipywidgets/widgets/widget.py in _handle_msg(self, msg)
    674                 if 'buffer_paths' in data:
    675                     _put_buffers(state, data['buffer_paths'], msg['buffers'])
--> 676                 self.set_state(state)
    677 
    678         # Handle a state request.

/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/ipywidgets/widgets/widget.py in set_state(self, sync_data)
    543                     from_json = self.trait_metadata(name, 'from_json',
    544                                                     self._trait_from_json)
--> 545                     self.set_trait(name, from_json(sync_data[name], self))
    546 
    547     def send(self, content, buffers=None):

/opt/anaconda3/envs/hsi/lib/python3.8/contextlib.py in __exit__(self, type, value, traceback)
    118         if type is None:
    119             try:
--> 120                 next(self.gen)
    121             except StopIteration:
    122                 return False

/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/traitlets/traitlets.py in hold_trait_notifications(self)
   1129                 for changes in cache.values():
   1130                     for change in changes:
-> 1131                         self.notify_change(change)
   1132 
   1133     def _notify_trait(self, name, old_value, new_value):

/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/ipywidgets/widgets/widget.py in notify_change(self, change)
    604                 # Send new state to front-end
    605                 self.send_state(key=name)
--> 606         super(Widget, self).notify_change(change)
    607 
    608     def __repr__(self):

/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/traitlets/traitlets.py in notify_change(self, change)
   1174                 c = getattr(self, c.name)
   1175 
-> 1176             c(change)
   1177 
   1178     def _add_notifiers(self, handler, name, type):

/opt/anaconda3/envs/hsi/lib/python3.8/site-packages/plotly/basewidget.py in _handler_js2py_pointsCallback(self, change)
    716         for trace_ind, trace_points_data in trace_points.items():
    717             points = Points(**trace_points_data)
--> 718             trace = self.data[trace_ind]
    719 
    720             if event_type == "plotly_click":

IndexError: tuple index out of range