Repository: napari/napari

Issue:
Error when editing shape data in nD
## 🐛 Bug

I have an image layer which can be n-dimensional, and then add a shapes layer that has the same dimensions. In 2D if I add a 2D rectangle in the shape layer, I can then modify its corresponding data e.g. I can round the coordinates ```viewer.layers['rois'].data = [np.around(x) for x in viewer.layers['rois'].data]```. With a higher dimensional image, I can add a rectangle without problem (and as expected it appears only at those specific coordinates where I add it) but I can't modify the underlying data. When I run the same command as above I get a long error message (see below). Weirdly, the underlying data do get changed (e.g. they are rounded) and when I interact with the viewer (moving sliders), the shape does actually change as expected. So it sounds like interacting with the viewer calls some function fixing the issues with the data axes, but that function is not called when directly modifying the data.

## To Reproduce

This works in 2D:

```python
import napari
import numpy as np

image = np.random.randint(0, 255, (16, 16))

viewer = napari.Viewer()
viewer.add_image(image)

target_ndims = viewer.layers[0].data.ndim

viewer.add_shapes(
            ndim = target_ndims,
            name='rois', edge_color='red', face_color=np.array([0,0,0,0]), edge_width=1)

viewer.layers['rois'].add([np.array([[1.7, 4.9], [1.7, 13.1], [13.5, 13.1], [13.5, 4.9]])])
viewer.layers['rois'].data = [np.around(x) for x in viewer.layers['rois'].data]
```
but not in 4D:

```python
import napari
import numpy as np

image = np.random.randint(0, 255, (3, 8, 16, 16))

viewer = napari.Viewer()
viewer.add_image(image)

target_ndims = viewer.layers[0].data.ndim

viewer.add_shapes(
            ndim = target_ndims,
            name='rois', edge_color='red', face_color=np.array([0,0,0,0]), edge_width=1)

viewer.layers['rois'].add([np.array([[1,4, 1.7, 4.9], [1,4,1.7, 13.1], [1,4,13.5, 13.1], [1,4,13.5, 4.9]])])
viewer.layers['rois'].data = [np.around(x) for x in viewer.layers['rois'].data]
```
and generated this error message:
```python
---------------------------------------------------------------------------
AxisError                                 Traceback (most recent call last)
Cell In [51], line 13
      8 viewer.add_shapes(
      9             ndim = target_ndims,
     10             name='rois', edge_color='red', face_color=np.array([0,0,0,0]), edge_width=1)
     12 viewer.layers['rois'].add([np.array([[1,4, 1.7, 4.9], [1,4,1.7, 13.1], [1,4,13.5, 13.1], [1,4,13.5, 4.9]])])
---> 13 viewer.layers['rois'].data = [np.around(x) for x in viewer.layers['rois'].data]

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/shapes.py:647, in Shapes.data(self, data)
    639     face_color = np.concatenate(
    640         (
    641             face_color,
    642             self._get_new_shape_color(n_shapes_difference, 'face'),
    643         )
    644     )
    646 self._data_view = ShapeList(ndisplay=self._ndisplay)
--> 647 self.add(
    648     data,
    649     shape_type=shape_type,
    650     edge_width=edge_widths,
    651     edge_color=edge_color,
    652     face_color=face_color,
    653     z_index=z_indices,
    654 )
    656 self._update_dims()
    657 self.events.data(value=self.data)

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/shapes.py:1985, in Shapes.add(self, data, shape_type, edge_width, edge_color, face_color, z_index)
   1983 self._feature_table.resize(total_shapes)
   1984 self.text.apply(self.features)
-> 1985 self._add_shapes(
   1986     data,
   1987     shape_type=shape_type,
   1988     edge_width=edge_width,
   1989     edge_color=edge_color,
   1990     face_color=face_color,
   1991     z_index=z_index,
   1992 )
   1993 self.events.data(value=self.data)

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/shapes.py:2192, in Shapes._add_shapes(self, data, shape_type, edge_width, edge_color, face_color, z_index, z_refresh)
   2182     # Turn input arguments into iterables
   2183     shape_inputs = zip(
   2184         data,
   2185         ensure_iterable(shape_type),
   (...)
   2189         ensure_iterable(z_index),
   2190     )
-> 2192     self._add_shapes_to_view(shape_inputs, self._data_view)
   2194 self._display_order_stored = copy(self._dims_order)
   2195 self._ndisplay_stored = copy(self._ndisplay)

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/shapes.py:2229, in Shapes._add_shapes_to_view(self, shape_inputs, data_view)
   2221 # Add all shapes at once (faster than adding them one by one)
   2222 data_view.add(
   2223     shape=shapes,
   2224     edge_color=edge_colors,
   2225     face_color=face_colors,
   2226     z_refresh=False,
   2227 )
-> 2229 data_view._update_z_order()

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/_shape_list.py:694, in ShapeList._update_z_order(self)
    690     triangles_z_order = [
    691         np.arange(idx[z], idx[z] + counts[z]) for z in self._z_order
    692     ]
    693     self._mesh.triangles_z_order = np.concatenate(triangles_z_order)
--> 694 self._update_displayed()

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/napari/layers/shapes/_shape_list.py:204, in ShapeList._update_displayed(self)
    201 # Slice key must exactly match mins and maxs of shape as then the
    202 # shape is entirely contained within the current slice.
    203 if len(self.shapes) > 0:
--> 204     self._displayed = np.all(self.slice_keys == slice_key, axis=(1, 2))
    205 else:
    206     self._displayed = []

File <__array_function__ internals>:180, in all(*args, **kwargs)

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/numpy/core/fromnumeric.py:2489, in all(a, axis, out, keepdims, where)
   2406 @array_function_dispatch(_all_dispatcher)
   2407 def all(a, axis=None, out=None, keepdims=np._NoValue, *, where=np._NoValue):
   2408     """
   2409     Test whether all array elements along a given axis evaluate to True.
   2410 
   (...)
   2487 
   2488     """
-> 2489     return _wrapreduction(a, np.logical_and, 'all', axis, None, out,
   2490                           keepdims=keepdims, where=where)

File ~/mambaforge/envs/napari_annotation/lib/python3.10/site-packages/numpy/core/fromnumeric.py:86, in _wrapreduction(obj, ufunc, method, axis, dtype, out, **kwargs)
     83         else:
     84             return reduction(axis=axis, out=out, **passkwargs)
---> 86 return ufunc.reduce(obj, axis, dtype, out, **passkwargs)

AxisError: axis 1 is out of bounds for array of dimension 0
```

## Expected behavior

Applying a function to the data of the shapes layer should work independently of the dimensionality.

## Environment

napari: 0.4.17
Platform: macOS-12.5.1-x86_64-i386-64bit
System: MacOS 12.5.1
Python: 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:43:32) [Clang 12.0.1 ]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.23.5
SciPy: 1.9.3
Dask: 2022.11.1
VisPy: 0.11.0
magicgui: 0.6.1
superqt: 0.4.0
in-n-out: 0.1.6
app-model: 0.1.1
npe2: 0.6.1

OpenGL:
- GL version: 2.1 Metal - 76.3
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1512x982, scale 2.0



Discussion:

Resolving patch:
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
@@ -655,6 +655,9 @@ def data(self, data):
             )
 
         self._data_view = ShapeList(ndisplay=self._slice_input.ndisplay)
+        self._data_view.slice_key = np.array(self._slice_indices)[
+            self._slice_input.not_displayed
+        ]
         self.add(
             data,
             shape_type=shape_type,


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
@@ -7,6 +7,7 @@
 from pydantic import ValidationError
 
 from napari._tests.utils import check_layer_world_data_extent
+from napari.components import ViewerModel
 from napari.layers import Shapes
 from napari.layers.utils._text_constants import Anchor
 from napari.layers.utils.color_encoding import ConstantColorEncoding
@@ -2166,3 +2167,32 @@ def test_set_data_3d():
     shapes = Shapes(lines, shape_type='line')
     shapes._slice_dims(ndisplay=3)
     shapes.data = lines
+
+
+def test_editing_4d():
+    viewer = ViewerModel()
+    viewer.add_shapes(
+        ndim=4,
+        name='rois',
+        edge_color='red',
+        face_color=np.array([0, 0, 0, 0]),
+        edge_width=1,
+    )
+
+    viewer.layers['rois'].add(
+        [
+            np.array(
+                [
+                    [1, 4, 1.7, 4.9],
+                    [1, 4, 1.7, 13.1],
+                    [1, 4, 13.5, 13.1],
+                    [1, 4, 13.5, 4.9],
+                ]
+            )
+        ]
+    )
+    # check if set data doe not end with an exception
+    # https://github.com/napari/napari/issues/5379
+    viewer.layers['rois'].data = [
+        np.around(x) for x in viewer.layers['rois'].data
+    ]