Repository: napari/napari

Issue:
Tracks color_by not handled correctly
## 🐛 Bug

Two issues with `color_by`:

1. When creating a tracks layer with multiple properties and specifying a specific property name using `color_by`, the property is not selected for coloring (it always seems to default to the same property). Color_by always seems to default to the first dictionary key. ![image](https://user-images.githubusercontent.com/2210044/95926667-8f33dc80-0e08-11eb-85a0-12a9d40b612a.png)
Looking at the Track `__init__` method I don't really see why that happens, as the property seems to be initialized [here](https://github.com/napari/napari/blob/5fff516d237a8786572474a36b41024dfb3267cc/napari/layers/tracks/tracks.py#L136) and [here](https://github.com/napari/napari/blob/5fff516d237a8786572474a36b41024dfb3267cc/napari/layers/tracks/tracks.py#L154).


1. The second issue is a silent failure when specifying a property name that does not exist as a dictionary key: https://github.com/napari/napari/blob/5fff516d237a8786572474a36b41024dfb3267cc/napari/layers/tracks/tracks.py#L426  I think the user should be notified. Not sure about the best method to do this but maybe just throw a `ValueError` ?
   

## To Reproduce

Steps to reproduce the behavior:

1. Create a track layer with a properties dictionary that contains multiple keys. 
2. Try different keys for `color_by`.

Apologies that I'm only showing a screenshot above. Need to generate some fake tracking data to make
a share-able code snippet.

## Expected behavior

The track should be colored by the property corresponding to the specified `color_by` key.

## 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
```

 

## Additional context

@quantumjot 


Discussion:
@VolkerH fyi the examples directory contains an example with synthetic data:

https://github.com/napari/napari/blob/d3511ee050eacdb00db9cbc119f346c0d13b24af/examples/tracks_3d.py
Thanks for the report! @kevinyamauchi might have ideas too
Thanks @jni 

I modified `tracks_3d.py` to create a reproducible example:
https://github.com/VolkerH/napari/blob/tracks_color_by.py/examples/tracks_3d.py

```
    speedlayer = viewer.add_tracks(tracks, properties=properties, name='tracks', color_by='speed')
    print(f"Speed layer color_by is: {speedlayer.color_by}.")
    distancelayer = viewer.add_tracks(tracks, properties=properties, name='tracks', color_by='distance')
    print(f"Distance layer color_by is: {distancelayer.color_by}")
```

produces:


```
volker@gpubox:~/Github/napari/examples$ python tracks_3d.py 
Speed layer color_by is: time.
Distance layer color_by is: time
```

I added a test in a branch https://github.com/VolkerH/napari/blob/tracks_tests/napari/layers/tracks/_tests/test_tracks.py

```
properties_dict = {'time': np.arange(100), 'other': np.arange(100)}
properties_df = pd.DataFrame(properties_dict)

def test_track_layer_color_by():
    """Test color_by selects specified property."""
    data = np.zeros((100, 4))
    data[:, 1] = np.arange(100)
    for key in properties_dict.keys():
        layer = Tracks(data, properties=properties_dict, name = key, color_by=key)
        print(layer.color_by, key)
        assert layer.color_by==key
```

The test passes. So it appears not to be an issue when `Track` is used, but an issue when `v.add_tracks(...)` is used.
But I don't know enough about how `add_tracks()` is dynamically generated:
![image](https://user-images.githubusercontent.com/2210044/95946775-f5d0ee80-0e38-11eb-97c5-4cd02bb5f76a.png)



Ok, so I suspect this error must be happening somewhere in
https://github.com/napari/napari/blob/be9196a02c08e3d6e38704ecc00beb170323f563/napari/components/add_layers_mixin.py

maybe around here ???:   
https://github.com/napari/napari/blob/be9196a02c08e3d6e38704ecc00beb170323f563/napari/components/add_layers_mixin.py#L448



My guess is that this originates from the `color_by` Qt events when the layer controls are built.
> My guess is that this originates from the color_by Qt events when the layer controls are built.

I think this is a good guess. I will take a look this afternoon before the dev meeting.

Resolving patch:
diff --git a/napari/_qt/layer_controls/qt_tracks_controls.py b/napari/_qt/layer_controls/qt_tracks_controls.py
--- a/napari/_qt/layer_controls/qt_tracks_controls.py
+++ b/napari/_qt/layer_controls/qt_tracks_controls.py
@@ -40,8 +40,9 @@ def __init__(self, layer):
         # keys
         self.color_by_combobox = QComboBox()
         self.color_by_combobox.addItems(self.layer.properties_to_color_by)
+
         self.colormap_combobox = QComboBox()
-        self.colormap_combobox.addItems(AVAILABLE_COLORMAPS.keys())
+        self.colormap_combobox.addItems(list(AVAILABLE_COLORMAPS.keys()))
 
         # slider for track tail length
         self.tail_length_slider = QSlider(Qt.Horizontal)
@@ -99,7 +100,6 @@ def __init__(self, layer):
 
         self._on_tail_length_change()
         self._on_tail_width_change()
-        self._on_properties_change()
         self._on_colormap_change()
         self._on_color_by_change()
 


Test patch:
diff --git a/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py b/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
new file mode 100644
--- /dev/null
+++ b/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
@@ -0,0 +1,35 @@
+import numpy as np
+from qtpy.QtCore import Qt
+
+from napari._qt.layer_controls.qt_tracks_controls import QtTracksControls
+from napari.layers import Tracks
+
+_TRACKS = np.zeros((2, 4))
+_PROPERTIES = {'speed': [50, 30], 'time': [0, 1]}
+
+
+def test_tracks_controls_color_by(qtbot):
+    """Check updating of the color_by combobox."""
+    inital_color_by = 'time'
+    layer = Tracks(_TRACKS, properties=_PROPERTIES, color_by=inital_color_by)
+    qtctrl = QtTracksControls(layer)
+    qtbot.addWidget(qtctrl)
+
+    # verify the color_by argument is initialized correctly
+    assert layer.color_by == inital_color_by
+    assert qtctrl.color_by_combobox.currentText() == inital_color_by
+
+    # update color_by from the layer model
+    layer_update_color_by = 'speed'
+    layer.color_by = layer_update_color_by
+    assert layer.color_by == layer_update_color_by
+    assert qtctrl.color_by_combobox.currentText() == layer_update_color_by
+
+    # update color_by from the qt controls
+    qt_update_color_by = 'track_id'
+    speed_index = qtctrl.color_by_combobox.findText(
+        qt_update_color_by, Qt.MatchFixedString
+    )
+    qtctrl.color_by_combobox.setCurrentIndex(speed_index)
+    assert layer.color_by == qt_update_color_by
+    assert qtctrl.color_by_combobox.currentText() == qt_update_color_by