Repository: napari/napari

Issue:
Simpler way to erase all data in shapes layer
## 🚀 Feature
An [image.sc post](https://forum.image.sc/t/proper-way-to-erase-all-data-in-a-layer/40702) requested a simpler way to erase all data in a shapes layer, possibly via

```python
layer.data = []
```
or
```python
layer.data = np.empty((0, 2))
```

See that post for additional discussion and suggestions, which we can continue here.


Discussion:
This issue has been mentioned on **Image.sc Forum**. There might be relevant details there:

https://forum.image.sc/t/proper-way-to-erase-all-data-in-a-layer/40702/5


Resolving patch:
diff --git a/napari/layers/shapes/_shapes_utils.py b/napari/layers/shapes/_shapes_utils.py
--- a/napari/layers/shapes/_shapes_utils.py
+++ b/napari/layers/shapes/_shapes_utils.py
@@ -1004,3 +1004,29 @@ def get_shape_ndim(data):
     else:
         ndim = np.array(data[0]).shape[1]
     return ndim
+
+
+def number_of_shapes(data):
+    """Determine number of shapes in the data.
+
+    Parameters
+    ----------
+    data : list or np.ndarray
+        Can either be no shapes, if empty, a
+        single shape or a list of shapes.
+
+    Returns
+    -------
+    n_shapes : int
+        Number of new shapes
+    """
+    if len(data) == 0:
+        # If no new shapes
+        n_shapes = 0
+    elif np.array(data[0]).ndim == 1:
+        # If a single array for a shape
+        n_shapes = 1
+    else:
+        n_shapes = len(data)
+
+    return n_shapes
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -50,7 +50,7 @@
     vertex_insert,
     vertex_remove,
 )
-from ._shapes_utils import create_box, get_shape_ndim
+from ._shapes_utils import create_box, get_shape_ndim, number_of_shapes
 
 DEFAULT_COLOR_CYCLE = np.array([[1, 0, 1, 1], [0, 1, 0, 1]])
 
@@ -521,11 +521,8 @@ def data(self, data, shape_type='rectangle'):
         self._data_view = ShapeList()
         self.add(data, shape_type=shape_type)
 
-        adding = len(data)
-        # handle case where data is just a single shape
-        if np.asarray(data[0]).ndim == 1:
-            adding = 1
-        self.text.add(self.current_properties, adding)
+        n_new_shapes = number_of_shapes(data)
+        self.text.add(self.current_properties, n_new_shapes)
 
         self._update_dims()
         self.events.data()
@@ -1441,11 +1438,7 @@ def add(
         if edge_width is None:
             edge_width = self.current_edge_width
 
-        if np.array(data[0]).ndim == 1:
-            # If a single array for a shape
-            n_new_shapes = 1
-        else:
-            n_new_shapes = len(data)
+        n_new_shapes = number_of_shapes(data)
 
         if edge_color is None:
             edge_color = self._get_new_shape_color(
@@ -1534,7 +1527,7 @@ def _init_shapes(
             shapes.
         """
 
-        n_shapes = len(data)
+        n_shapes = number_of_shapes(data)
         with self.block_update_properties():
             self._edge_color_property = ''
             self.edge_color_cycle_map = {}


Test patch:
diff --git a/napari/layers/shapes/_tests/test_shapes.py b/napari/layers/shapes/_tests/test_shapes.py
--- a/napari/layers/shapes/_tests/test_shapes.py
+++ b/napari/layers/shapes/_tests/test_shapes.py
@@ -600,6 +600,28 @@ def test_selecting_shapes():
     assert layer.selected_data == set()
 
 
+def test_removing_all_shapes_empty_list():
+    """Test removing all shapes with an empty list."""
+    data = 20 * np.random.random((10, 4, 2))
+    np.random.seed(0)
+    layer = Shapes(data)
+    assert layer.nshapes == 10
+
+    layer.data = []
+    assert layer.nshapes == 0
+
+
+def test_removing_all_shapes_empty_array():
+    """Test removing all shapes with an empty list."""
+    data = 20 * np.random.random((10, 4, 2))
+    np.random.seed(0)
+    layer = Shapes(data)
+    assert layer.nshapes == 10
+
+    layer.data = np.empty((0, 2))
+    assert layer.nshapes == 0
+
+
 def test_removing_selected_shapes():
     """Test removing selected shapes."""
     np.random.seed(0)
@@ -849,7 +871,6 @@ def test_color_direct(attribute: str):
     color_array[list(selected_data)] = colorarray_green
     layer_color = getattr(layer, f'{attribute}_color')
     np.testing.assert_allclose(color_array, layer_color)
-
     # Add new shape and test its color
     new_shape = np.random.random((1, 4, 2))
     layer.selected_data = set()
@@ -878,6 +899,19 @@ def test_color_direct(attribute: str):
     np.testing.assert_allclose(color_array, layer_color)
 
 
+@pytest.mark.parametrize("attribute", ['edge', 'face'])
+def test_single_shape_properties(attribute):
+    """Test creating single shape with properties"""
+    shape = (4, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    layer_kwargs = {f'{attribute}_color': 'red'}
+    layer = Shapes(data, **layer_kwargs)
+    layer_color = getattr(layer, f'{attribute}_color')
+    assert len(layer_color) == 1
+    np.testing.assert_allclose([1, 0, 0, 1], layer_color[0])
+
+
 color_cycle_str = ['red', 'blue']
 color_cycle_rgb = [[1, 0, 0], [0, 0, 1]]
 color_cycle_rgba = [[1, 0, 0, 1], [0, 0, 1, 1]]
diff --git a/napari/layers/shapes/_tests/test_shapes_utils.py b/napari/layers/shapes/_tests/test_shapes_utils.py
new file mode 100644
--- /dev/null
+++ b/napari/layers/shapes/_tests/test_shapes_utils.py
@@ -0,0 +1,19 @@
+import numpy as np
+
+from napari.layers.shapes._shapes_utils import number_of_shapes
+
+
+def test_no_shapes():
+    """Test no shapes."""
+    assert number_of_shapes([]) == 0
+    assert number_of_shapes(np.empty((0, 4, 2))) == 0
+
+
+def test_one_shape():
+    """Test one shape."""
+    assert number_of_shapes(np.random.random((4, 2))) == 1
+
+
+def test_many_shapes():
+    """Test many shapes."""
+    assert number_of_shapes(np.random.random((8, 4, 2))) == 8