Repository: napari/napari

Issue:
Recently merged  Affine PR causes AttributeError: 'VispyTracksLayer' object has no attribute '_on_scale_change'
## 🐛 Bug

With the  merging of #1616 into master (@jni, @sofroniewn), a bug appeared when adding a tracks layer.
The bug does not appear with the last master branch prior to the merge.

## To Reproduce

Steps to reproduce the behavior:

Haven't made a generic example yet, but I submitted the following in a notebook

```
v = napari.Viewer()
v.add_tracks(tracks, name='ilastik tracks')
```

and ended up with this error:


```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-10-05f52b983105> in <module>
      1 v = napari.Viewer()
----> 2 v.add_tracks(tracks, name='ilastik tracks')

<string> in add_tracks(self, data, properties, graph, tail_width, tail_length, name, metadata, scale, translate, opacity, blending, visible, colormap, color_by, colormaps_dict)

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/components/add_layers_mixin.py in add_layer(self, layer)
     48         layer.events.cursor_size.connect(self._update_cursor_size)
     49         layer.events.data.connect(self._on_layers_change)
---> 50         self.layers.append(layer)
     51         self._update_layers(layers=[layer])
     52 

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/utils/list/_model.py in append(self, obj)
     57     def append(self, obj):
     58         TypedList.append(self, obj)
---> 59         self.events.added(item=obj, index=len(self) - 1)
     60 
     61     def pop(self, key):

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/utils/events/event.py in __call__(self, *args, **kwargs)
    512                     continue
    513 
--> 514                 self._invoke_callback(cb, event)
    515                 if event.blocked:
    516                     break

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/utils/events/event.py in _invoke_callback(self, cb, event)
    533                 self.print_callback_errors,
    534                 self,
--> 535                 cb_event=(cb, event),
    536             )
    537 

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/utils/events/event.py in _invoke_callback(self, cb, event)
    527     def _invoke_callback(self, cb, event):
    528         try:
--> 529             cb(event)
    530         except Exception:
    531             _handle_exception(

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/_qt/qt_viewer.py in _add_layer(self, event)
    281         layers = event.source
    282         layer = event.item
--> 283         vispy_layer = create_vispy_visual(layer)
    284         vispy_layer.node.parent = self.view.scene
    285         vispy_layer.order = len(layers) - 1

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/_vispy/utils.py in create_vispy_visual(layer)
     32     for layer_type, visual in layer_to_visual.items():
     33         if isinstance(layer, layer_type):
---> 34             return visual(layer)
     35 
     36     raise TypeError(

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/_vispy/vispy_tracks_layer.py in __init__(self, layer)
     48         self._reset_base()
     49 
---> 50         self._on_data_change()
     51         self._on_appearance_change()
     52 

~/miniconda3/envs/fastai/lib/python3.7/site-packages/napari/_vispy/vispy_tracks_layer.py in _on_data_change(self, event)
     66         self.node.update()
     67         # Call to update order of translation values with new dims:
---> 68         self._on_scale_change()
     69         self._on_translate_change()
     70 

AttributeError: 'VispyTracksLayer' object has no attribute '_on_scale_change'
```


## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

```
napari: 0.3.9.dev50+ge99cb1e
Platform: Linux-5.4.0-51-generic-x86_64-with-debian-bullseye-sid
Python: 3.7.7 (default, May 7 2020, 21:25:33) [GCC 7.3.0]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.19.1
SciPy: 1.5.2
Dask: 2.25.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 450.66
MAX_TEXTURE_SIZE: 32768

Plugins:
- napari_plugin_engine: 0.1.7
- svg: 0.1.4
```
 - Any other relevant information:



Discussion:

Resolving patch:
diff --git a/napari/_vispy/vispy_tracks_layer.py b/napari/_vispy/vispy_tracks_layer.py
--- a/napari/_vispy/vispy_tracks_layer.py
+++ b/napari/_vispy/vispy_tracks_layer.py
@@ -5,7 +5,7 @@
 
 
 class VispyTracksLayer(VispyBaseLayer):
-    """ VispyTracksLayer
+    """VispyTracksLayer
 
     Track layer for visualizing tracks.
 
@@ -65,8 +65,7 @@ def _on_data_change(self, event=None):
 
         self.node.update()
         # Call to update order of translation values with new dims:
-        self._on_scale_change()
-        self._on_translate_change()
+        self._on_matrix_change()
 
     def _on_appearance_change(self, event=None):
         """ change the appearance of the data """
@@ -104,8 +103,7 @@ def _on_tracks_change(self, event=None):
         )
 
         # Call to update order of translation values with new dims:
-        self._on_scale_change()
-        self._on_translate_change()
+        self._on_matrix_change()
 
     def _on_graph_change(self, event=None):
         """ update the shader when the graph data changes """
@@ -131,5 +129,4 @@ def _on_graph_change(self, event=None):
         )
 
         # Call to update order of translation values with new dims:
-        self._on_scale_change()
-        self._on_translate_change()
+        self._on_matrix_change()
diff --git a/napari/layers/tracks/tracks.py b/napari/layers/tracks/tracks.py
--- a/napari/layers/tracks/tracks.py
+++ b/napari/layers/tracks/tracks.py
@@ -55,6 +55,15 @@ class Tracks(Layer):
         Scale factors for the layer.
     translate : tuple of float
         Translation values for the layer.
+    rotate : float, 3-tuple of float, or n-D array.
+        If a float convert into a 2D rotation matrix using that value as an
+        angle. If 3-tuple convert into a 3D rotation matrix, using a yaw,
+        pitch, roll convention. Otherwise assume an nD rotation. Angles are
+        assumed to be in degrees. They can be converted from radians with
+        np.degrees if needed.
+    shear : 1-D array or n-D array
+        Either a vector of upper triangular values, or an nD shear matrix with
+        ones along the main diagonal.
     opacity : float
         Opacity of the layer visual, between 0.0 and 1.0.
     blending : str
@@ -83,6 +92,9 @@ def __init__(
         metadata=None,
         scale=None,
         translate=None,
+        rotate=None,
+        shear=None,
+        affine=None,
         opacity=1,
         blending='additive',
         visible=True,
@@ -112,6 +124,9 @@ def __init__(
             metadata=metadata,
             scale=scale,
             translate=translate,
+            rotate=rotate,
+            shear=shear,
+            affine=affine,
             opacity=opacity,
             blending=blending,
             visible=visible,
@@ -194,11 +209,9 @@ def _get_state(self):
                 'data': self.data,
                 'properties': self.properties,
                 'graph': self.graph,
-                'display_id': self.display_id,
-                'display_tail': self.display_tail,
-                'display_graph': self.display_graph,
                 'color_by': self.color_by,
                 'colormap': self.colormap,
+                'colormaps_dict': self.colormaps_dict,
                 'tail_width': self.tail_width,
                 'tail_length': self.tail_length,
             }


Test patch:
diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py
--- a/napari/_qt/_tests/test_qt_viewer.py
+++ b/napari/_qt/_tests/test_qt_viewer.py
@@ -56,7 +56,7 @@ def test_qt_viewer_toggle_console(make_test_viewer):
 
 @pytest.mark.parametrize('layer_class, data, ndim', layer_test_data)
 def test_add_layer(make_test_viewer, layer_class, data, ndim):
-    viewer = make_test_viewer(ndisplay=ndim)
+    viewer = make_test_viewer(ndisplay=int(np.clip(ndim, 2, 3)))
     view = viewer.window.qt_viewer
 
     add_layer_by_type(viewer, layer_class, data)
diff --git a/napari/_tests/utils.py b/napari/_tests/utils.py
--- a/napari/_tests/utils.py
+++ b/napari/_tests/utils.py
@@ -1,7 +1,15 @@
 import numpy as np
 
 from napari import Viewer
-from napari.layers import Image, Labels, Points, Shapes, Surface, Vectors
+from napari.layers import (
+    Image,
+    Labels,
+    Points,
+    Shapes,
+    Surface,
+    Tracks,
+    Vectors,
+)
 
 """
 Used as pytest params for testing layer add and view functionality (Layer class, data, ndim)
@@ -9,6 +17,7 @@
 layer_test_data = [
     (Image, np.random.random((10, 15)), 2),
     (Image, np.random.random((10, 15, 20)), 3),
+    (Image, np.random.random((5, 10, 15, 20)), 4),
     (Image, [np.random.random(s) for s in [(40, 20), (20, 10), (10, 5)]], 2),
     (Labels, np.random.randint(20, size=(10, 15)), 2),
     (Labels, np.random.randint(20, size=(6, 10, 15)), 3),
@@ -26,10 +35,24 @@
         ),
         3,
     ),
+    (
+        Tracks,
+        np.column_stack(
+            (np.ones(20), np.arange(20), 20 * np.random.random((20, 2)))
+        ),
+        3,
+    ),
+    (
+        Tracks,
+        np.column_stack(
+            (np.ones(20), np.arange(20), 20 * np.random.random((20, 3)))
+        ),
+        4,
+    ),
 ]
 
 
-classes = [Labels, Points, Vectors, Shapes, Surface, Image]
+classes = [Labels, Points, Vectors, Shapes, Surface, Tracks, Image]
 names = [cls.__name__.lower() for cls in classes]
 layer2addmethod = {
     cls: getattr(Viewer, 'add_' + name) for cls, name in zip(classes, names)