Repository: napari/napari

Issue:
tracks 'color_by' attribute not maintained after updating properties
## 🐛 Bug

The `color_by` attribute on a tracks layer changes if the properties are updated. 

## To Reproduce

```python
import napari
from skimage import data
import numpy as np

hubble_image = data.hubble_deep_field()

tracks_data = np.asarray([
    [1, 0, 236, 0],
    [1, 1, 236, 100],
    [1, 2, 236, 200],
    [1, 3, 236, 500],
    [1, 4, 236, 1000],
    [2, 0, 436, 0],
    [2, 1, 436, 100],
    [2, 2, 436, 200],
    [2, 3, 436, 500],
    [2, 4, 436, 1000],
    [3, 0, 636, 0],
    [3, 1, 636, 100],
    [3, 2, 636, 200],
    [3, 3, 636, 500],
    [3, 4, 636, 1000]
])
track_confidence = np.array(5*[0.9] + 5*[0.3] + 5 * [0.1])
properties = {
    'time': tracks_data[:, 1],
    'confidence': track_confidence
}

viewer = napari.view_image(hubble_image)
viewer.add_tracks(tracks_data, properties=properties)
napari.run()
```

then in the console

```ipython
viewer.layers['tracks_data'].color_by
Out[1]: 'track_id'

viewer.layers['tracks_data'].properties = viewer.layers['tracks_data'].properties

viewer.layers['tracks_data'].color_by
Out[3]: 'time'
```
## Expected behavior

If the previous 'color_by' attribute is still present in the properties, this should not be changed by updating the properties

## Additional context

The offending lines of code are 
https://github.com/napari/napari/blob/cd5c3147a13a42304a6e26a56f516bdc8b879557/napari/_qt/layer_controls/qt_tracks_controls.py#L128-L130



Discussion:
Hello Alisterburt can I make a try their.
It will my 1st contribution to ML lib.

@Kd-Here cool! Feel free to drop a message here if you get stuck

The general implementation would probably look like
- store the current 'color_by' attribute before clearing the combobox
- recreate the combobox (exactly as done in the 'offending lines of code')
- check if the previously stored 'color_by' is present in the `self.layer.properties_to_color_by`
- if present, set `self.layers.color_by` to the previously stored value
Hey @alisterburt Thanks for detail informatn!!!
From 2 days I was checking & using napari and **amazed by your work.** 👏

I implemented the code as mention above. 
![image](https://user-images.githubusercontent.com/90677747/202175699-d3a707ce-5312-4852-b5b2-9960d757ea2b.png)

But it is not working correctly. (I know it's my mistake I still don't correctly knowt the class used n their attributes)
Do I need to make changes in napari/layers/tracks/tracks.py file https://github.com/napari/napari/commit/88db2f064b1d19a7160560863932ea4ea78474ab

It's possible that my work replacing `properties` with `features` makes this more confusing than it needs to be, so happy to help out if that's the case.

@Kd-Here : do you have a work-in-progress PR or branch you could share? I think you might just need to swap 133 and 134 in the image above because assigning `Tracks.color_by` to something will emit the `Tracks.events.color_by` event which will set the current index/text of the combobox here. 

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
@@ -123,10 +123,10 @@ def _on_head_length_change(self):
 
     def _on_properties_change(self):
         """Change the properties that can be used to color the tracks."""
-        with self.layer.events.properties.blocker():
-            with qt_signals_blocked(self.color_by_combobox):
-                self.color_by_combobox.clear()
+        with qt_signals_blocked(self.color_by_combobox):
+            self.color_by_combobox.clear()
             self.color_by_combobox.addItems(self.layer.properties_to_color_by)
+        self._on_color_by_change()
 
     def _on_colormap_change(self):
         """Receive layer model colormap change event and update combobox."""
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
@@ -410,8 +410,8 @@ def features(
         features: Union[Dict[str, np.ndarray], pd.DataFrame],
     ) -> None:
         self._manager.features = features
-        self.events.properties()
         self._check_color_by_in_features()
+        self.events.properties()
 
     @property
     def properties(self) -> Dict[str, np.ndarray]:


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
--- a/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_tracks_layer.py
@@ -1,3 +1,5 @@
+from typing import Dict, List
+
 import numpy as np
 import pytest
 from qtpy.QtCore import Qt
@@ -5,16 +7,27 @@
 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]}
 
+@pytest.fixture
+def null_data() -> np.ndarray:
+    return np.zeros((2, 4))
+
+
+@pytest.fixture
+def properties() -> Dict[str, List]:
+    return {
+        'track_id': [0, 0],
+        'time': [0, 0],
+        'speed': [50, 30],
+    }
 
-def test_tracks_controls_color_by(qtbot):
+
+def test_tracks_controls_color_by(null_data, properties, qtbot):
     """Check updating of the color_by combobox."""
     inital_color_by = 'time'
     with pytest.warns(UserWarning) as wrn:
         layer = Tracks(
-            _TRACKS, properties=_PROPERTIES, color_by=inital_color_by
+            null_data, properties=properties, color_by=inital_color_by
         )
     assert "Previous color_by key 'time' not present" in str(wrn[0].message)
     qtctrl = QtTracksControls(layer)
@@ -38,3 +51,48 @@ def test_tracks_controls_color_by(qtbot):
     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
+
+
+@pytest.mark.parametrize('color_by', ('track_id', 'speed'))
+def test_color_by_same_after_properties_change(
+    null_data, properties, color_by, qtbot
+):
+    """See https://github.com/napari/napari/issues/5330"""
+    layer = Tracks(null_data, properties=properties)
+    layer.color_by = color_by
+    controls = QtTracksControls(layer)
+    qtbot.addWidget(controls)
+    assert controls.color_by_combobox.currentText() == color_by
+
+    # Change the properties value by removing the time column.
+    layer.properties = {
+        'track_id': properties['track_id'],
+        'speed': properties['speed'],
+    }
+
+    assert layer.color_by == color_by
+    assert controls.color_by_combobox.currentText() == color_by
+
+
+def test_color_by_missing_after_properties_change(
+    null_data, properties, qtbot
+):
+    """See https://github.com/napari/napari/issues/5330"""
+    layer = Tracks(null_data, properties=properties)
+    layer.color_by = 'time'
+    controls = QtTracksControls(layer)
+    qtbot.addWidget(controls)
+    assert controls.color_by_combobox.currentText() == 'time'
+
+    # Change the properties value by removing the time column.
+    with pytest.warns(
+        UserWarning,
+        match="Previous color_by key 'time' not present in features. Falling back to track_id",
+    ):
+        layer.properties = {
+            'track_id': properties['track_id'],
+            'speed': properties['speed'],
+        }
+
+    assert layer.color_by == 'track_id'
+    assert controls.color_by_combobox.currentText() == 'track_id'