Repository: napari/napari

Issue:
Deleting points data is broken with an index error
## 🐛 Bug

Using git bisect I tracked the problem to #2204 (sorry @kevinyamauchi 😬), which broke resetting of a Points layer data, which now raises an IndexError (see below).

## To Reproduce

```python
import numpy as np
import napari

viewer = napari.Viewer()
points = np.random.random((4, 2)) * 4

pts_layer = viewer.add_points(points)
pts_layer.data = np.zeros([0, 2])
```

raises:

```pytb
Traceback (most recent call last):
  File "test-points-delete.py", line 8, in <module>
    pts_layer.data = np.zeros([0, 2])
  File "/Users/jni/projects/napari/napari/layers/points/points.py", line 422, in data
    self._edge._remove(
  File "/Users/jni/projects/napari/napari/layers/utils/color_manager.py", line 330, in _remove
    self.colors = np.delete(self.colors, selected_indices, axis=0)
  File "/Users/jni/projects/napari/napari/utils/events/evented_model.py", line 154, in __setattr__
    getattr(self.events, name)(value=after)  # emit event
  File "/Users/jni/projects/napari/napari/utils/events/event.py", line 550, in __call__
    self._invoke_callback(cb, event)
  File "/Users/jni/projects/napari/napari/utils/events/event.py", line 567, in _invoke_callback
    _handle_exception(
  File "/Users/jni/projects/napari/napari/utils/events/event.py", line 565, in _invoke_callback
    cb(event)
  File "/Users/jni/projects/napari/napari/_vispy/vispy_points_layer.py", line 41, in _on_data_change
    edge_color = self.layer._view_edge_color
  File "/Users/jni/projects/napari/napari/layers/points/points.py", line 1187, in _view_edge_color
    return self.edge_color[self._indices_view]
IndexError: index 0 is out of bounds for axis 0 with size 0
```

Interestingly, the below doesn't have any issues:

```python
import numpy as np
import napari

points = np.random.random((4, 2)) * 4

pts_layer = napari.layers.Points(points)
pts_layer.data = np.zeros([0, 2])
```

Which suggests that it's the event callback system that's broken, *not* the points layer itself. That would explain why this got past testing.

## Expected behavior

The color elements should be removed together with the data.

## Environment

```
napari: 0.4.6.dev45+geaa54747.d20210303
Platform: macOS-10.15.7-x86_64-i386-64bit
System: MacOS 10.15.7
Python: 3.8.2 (default, Mar 26 2020, 10:43:30)  [Clang 4.0.1 (tags/RELEASE_401/final)]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.19.2
SciPy: 1.5.1
Dask: 2021.01.1
VisPy: 0.6.5.dev320+gc275797b

OpenGL:
  - GL version:  2.1 ATI-3.10.19
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 2048x1152, scale 2.0
  - screen 2: resolution 1536x960, scale 2.0

Plugins:
  - affinder: 0.1.dev16+g189efd8
  - animation: 0.1.dev78+g5716ec8
  - btrack-reader: 0.1.0
  - console: 0.0.3
  - napari-sentinel-zip: 0.2.2
  - ome_zarr: 0.0.18.dev0
  - s3zarr: 0.0.2
  - svg: 0.1.4
  - tracks: 0.0.2
```




Discussion:
I'll also take this opportunity to thank @tlambert03 for all his work cleaning up our Qt launching which makes testing these things way cleaner!

Resolving patch:
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -414,55 +414,60 @@ def data(self, data: Optional[np.ndarray]):
         self._data = data
 
         # Adjust the size array when the number of points has changed
-        if len(data) < cur_npoints:
-            # If there are now fewer points, remove the size and colors of the
-            # extra ones
-            with self.events.set_data.blocker():
-                if len(self._edge.colors) > len(data):
-                    self._edge._remove(
-                        np.arange(len(data), len(self._edge.colors))
-                    )
-                if len(self._face.colors) > len(data):
-                    self._face._remove(
-                        np.arange(len(data), len(self._face.colors))
-                    )
-                self._size = self._size[: len(data)]
-
-                for k in self.properties:
-                    self.properties[k] = self.properties[k][: len(data)]
-
-        elif len(data) > cur_npoints:
-            # If there are now more points, add the size and colors of the
-            # new ones
-            with self.events.set_data.blocker():
-                adding = len(data) - cur_npoints
-                if len(self._size) > 0:
-                    new_size = copy(self._size[-1])
-                    for i in self._dims_displayed:
-                        new_size[i] = self.current_size
-                else:
-                    # Add the default size, with a value for each dimension
-                    new_size = np.repeat(
-                        self.current_size, self._size.shape[1]
-                    )
-                size = np.repeat([new_size], adding, axis=0)
-
-                for k in self.properties:
-                    new_property = np.repeat(
-                        self.current_properties[k], adding, axis=0
-                    )
-                    self.properties[k] = np.concatenate(
-                        (self.properties[k], new_property), axis=0
-                    )
-
-                # add new colors
-                self._edge._add(n_colors=adding)
-                self._face._add(n_colors=adding)
-
-                self.size = np.concatenate((self._size, size), axis=0)
-                self.selected_data = set(np.arange(cur_npoints, len(data)))
+        with self.events.blocker_all():
+            with self._edge.events.blocker_all():
+                with self._face.events.blocker_all():
+                    if len(data) < cur_npoints:
+                        # If there are now fewer points, remove the size and colors of the
+                        # extra ones
+                        if len(self._edge.colors) > len(data):
+                            self._edge._remove(
+                                np.arange(len(data), len(self._edge.colors))
+                            )
+                        if len(self._face.colors) > len(data):
+                            self._face._remove(
+                                np.arange(len(data), len(self._face.colors))
+                            )
+                        self._size = self._size[: len(data)]
+
+                        for k in self.properties:
+                            self.properties[k] = self.properties[k][
+                                : len(data)
+                            ]
+
+                    elif len(data) > cur_npoints:
+                        # If there are now more points, add the size and colors of the
+                        # new ones
+                        adding = len(data) - cur_npoints
+                        if len(self._size) > 0:
+                            new_size = copy(self._size[-1])
+                            for i in self._dims_displayed:
+                                new_size[i] = self.current_size
+                        else:
+                            # Add the default size, with a value for each dimension
+                            new_size = np.repeat(
+                                self.current_size, self._size.shape[1]
+                            )
+                        size = np.repeat([new_size], adding, axis=0)
+
+                        for k in self.properties:
+                            new_property = np.repeat(
+                                self.current_properties[k], adding, axis=0
+                            )
+                            self.properties[k] = np.concatenate(
+                                (self.properties[k], new_property), axis=0
+                            )
+
+                        # add new colors
+                        self._edge._add(n_colors=adding)
+                        self._face._add(n_colors=adding)
+
+                        self.size = np.concatenate((self._size, size), axis=0)
+                        self.selected_data = set(
+                            np.arange(cur_npoints, len(data))
+                        )
 
-                self.text.add(self.current_properties, adding)
+                        self.text.add(self.current_properties, adding)
 
         self._update_dims()
         self.events.data(value=self.data)


Test patch:
diff --git a/napari/_tests/test_viewer.py b/napari/_tests/test_viewer.py
--- a/napari/_tests/test_viewer.py
+++ b/napari/_tests/test_viewer.py
@@ -196,3 +196,13 @@ def test_toggling_scale_bar(make_napari_viewer):
     # Make scale bar not visible
     viewer.scale_bar.visible = False
     assert not viewer.scale_bar.visible
+
+
+def test_removing_points_data(make_napari_viewer):
+    viewer = make_napari_viewer()
+    points = np.random.random((4, 2)) * 4
+
+    pts_layer = viewer.add_points(points)
+    pts_layer.data = np.zeros([0, 2])
+
+    assert len(pts_layer.data) == 0