Repository: napari/napari

Issue:
Tracks should give informative error/warning when color_by is invalid
## 🐛 Bug

As reported by @volkerh in #1722: 

> The separate problem I flagged (I should have opened a separate issue)
is dealing with property names passed as color_by that don't exist as keys in
the properties. How should that behandled, raise a ValueError (I'd be happy to the PR)
or just issue a warning?

## To Reproduce

Steps to reproduce the behavior:

```python

viewer = napari.view_image(np.random.random((100, 100))

tracks_data = [
    [0, 0, 10, 10, 10],
    [0, 1, 10, 10, 20],
    [1, 0, 5, 8, 10],
    [1, 1, 10, 8, 7]
]
tracks_props = {
    'class': ['A', 'A', 'B', 'B']
}
viewer.add_tracks(tracks_data, properties=tracks_props, color_by='not_a_key')

```

## Expected behavior

We should give an informative error message.

I think there are two scenarios we need to handle:
1. the user passes a key to `color_by` that doesn't exist in properties. In this case, I think we should raise a `ValueError` (as @volkerh suggested).
2. the user changes the properties dictionary and the key in `color_by`, which was previously valid, no longer exists. In this case, I think we should take the "first" key in the new properties dictionary and issue a warning

I think both cases could be handled in the setter methods for the `Tracks.color_by` and `Tracks.properties`.

## Environment
napari: 0.3.9.dev40+gd3511ee
Platform: Linux-5.4.0-48-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



Discussion:
Hey, @volkerh! Sorry I missed this second issue in #1722! I have moved it here. If you're still interested, a PR would be greatly appreciated! Please let us know if you have any questions or comments.

edit: I have added my thoughts on how I think the behavior should be in the "Expected behavior" section
Thanks for moving this into a separate issue. Will start on a PR to address this.

Resolving patch:
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
@@ -3,6 +3,7 @@
 # from napari.utils.colormaps import AVAILABLE_COLORMAPS
 
 from typing import Dict, List, Union
+from warnings import warn
 
 import numpy as np
 
@@ -14,7 +15,7 @@
 
 
 class Tracks(Layer):
-    """ Tracks layer.
+    """Tracks layer.
 
     Parameters
     ----------
@@ -303,8 +304,8 @@ def current_time(self):
 
     @property
     def use_fade(self) -> bool:
-        """ toggle whether we fade the tail of the track, depending on whether
-        the time dimension is displayed """
+        """toggle whether we fade the tail of the track, depending on whether
+        the time dimension is displayed"""
         return 0 in self.dims.not_displayed
 
     @property
@@ -346,6 +347,15 @@ def properties_to_color_by(self) -> List[str]:
     @properties.setter
     def properties(self, properties: Dict[str, np.ndarray]):
         """ set track properties """
+        if self._color_by not in [*properties.keys(), 'track_id']:
+            warn(
+                (
+                    f"Previous color_by key {self._color_by} not present in"
+                    " new properties. Falling back to track_id"
+                ),
+                UserWarning,
+            )
+            self._color_by = 'track_id'
         self._manager.properties = properties
         self.events.properties()
         self.events.color_by()
@@ -423,7 +433,7 @@ def color_by(self) -> str:
     def color_by(self, color_by: str):
         """ set the property to color vertices by """
         if color_by not in self.properties_to_color_by:
-            return
+            raise ValueError(f'{color_by} is not a valid property key')
         self._color_by = color_by
         self._recolor_tracks()
         self.events.color_by()
@@ -482,8 +492,8 @@ def track_connex(self) -> np.ndarray:
 
     @property
     def track_colors(self) -> np.ndarray:
-        """ return the vertex colors according to the currently selected
-        property """
+        """return the vertex colors according to the currently selected
+        property"""
         return self._track_colors
 
     @property


Test patch:
diff --git a/napari/layers/tracks/_tests/test_tracks.py b/napari/layers/tracks/_tests/test_tracks.py
--- a/napari/layers/tracks/_tests/test_tracks.py
+++ b/napari/layers/tracks/_tests/test_tracks.py
@@ -71,6 +71,34 @@ def test_track_layer_properties(properties):
         np.testing.assert_equal(layer.properties[k], v)
 
 
+@pytest.mark.filterwarnings("ignore:.*track_id.*:UserWarning")
+def test_track_layer_colorby_nonexistant():
+    """Test error handling for non-existant properties with color_by"""
+    data = np.zeros((100, 4))
+    data[:, 1] = np.arange(100)
+    non_existant_property = 'not_a_valid_key'
+    assert non_existant_property not in properties_dict.keys()
+    with pytest.raises(ValueError):
+        Tracks(
+            data, properties=properties_dict, color_by=non_existant_property
+        )
+
+
+@pytest.mark.filterwarnings("ignore:.*track_id.*:UserWarning")
+def test_track_layer_properties_changed_colorby():
+    """Test behaviour when changes to properties invalidate current color_by"""
+    properties_dict_1 = {'time': np.arange(100), 'prop1': np.arange(100)}
+    properties_dict_2 = {'time': np.arange(100), 'prop2': np.arange(100)}
+    data = np.zeros((100, 4))
+    data[:, 1] = np.arange(100)
+    layer = Tracks(data, properties=properties_dict_1, color_by='prop1')
+    # test warning is raised
+    with pytest.warns(UserWarning):
+        layer.properties = properties_dict_2
+    # test default fallback
+    assert layer.color_by == 'track_id'
+
+
 def test_track_layer_graph():
     """Test track layer graph."""
     data = np.zeros((100, 4))