diff --git a/glue/core/data.py b/glue/core/data.py index 5293a488b..43e275929 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -1543,7 +1543,7 @@ def update_components(self, mapping): # alert hub of the change if self.hub is not None: - msg = NumericalDataChangedMessage(self) + msg = NumericalDataChangedMessage(self, components_changed=list(mapping.keys())) self.hub.broadcast(msg) for subset in self.subsets: diff --git a/glue/core/layer_artist.py b/glue/core/layer_artist.py index b7b5b8756..aa24e956b 100644 --- a/glue/core/layer_artist.py +++ b/glue/core/layer_artist.py @@ -228,6 +228,12 @@ def _check_subset_state_changed(self): self._changed = True self._state = state + def _on_components_changed(self, components_changed): + """ + React to a change to one or more of the components in this layer. + """ + pass + def __str__(self): return "%s for %s" % (self.__class__.__name__, self.layer.label) diff --git a/glue/core/message.py b/glue/core/message.py index 94302dd7c..68615ddd1 100644 --- a/glue/core/message.py +++ b/glue/core/message.py @@ -182,7 +182,9 @@ def __init__(self, sender, attribute, tag=None): class NumericalDataChangedMessage(DataMessage): - pass + def __init__(self, sender, components_changed=None, tag=None): + super(NumericalDataChangedMessage, self).__init__(sender, tag=tag) + self.components_changed = components_changed class DataCollectionMessage(Message): diff --git a/glue/core/tests/test_components_changed.py b/glue/core/tests/test_components_changed.py new file mode 100644 index 000000000..d729c97d7 --- /dev/null +++ b/glue/core/tests/test_components_changed.py @@ -0,0 +1,45 @@ +""" +Test that data.update_components() sends a NumericalDataChangedMessage +that conveys which components have been changed. +""" +from glue.core.data import Data +from glue.core.hub import HubListener +from glue.core.data_collection import DataCollection +from glue.core.message import NumericalDataChangedMessage + +import numpy as np +from numpy.testing import assert_array_equal + + +def test_message_carries_components(): + + test_data = Data(x=np.array([1, 2, 3, 4, 5]), y=np.array([1, 2, 3, 4, 5]), label='test_data') + data_collection = DataCollection([test_data]) + + class CustomListener(HubListener): + + def __init__(self, hub): + self.received = 0 + self.components_changed = None + hub.subscribe(self, NumericalDataChangedMessage, + handler=self.receive_message) + + def receive_message(self, message): + self.received += 1 + try: + self.components_changed = message.components_changed + except AttributeError: + self.components_changed = None + + listener = CustomListener(data_collection.hub) + assert listener.received == 0 + assert listener.components_changed is None + + cid_to_change = test_data.id['x'] + new_data = [5, 2, 6, 7, 10] + test_data.update_components({cid_to_change: new_data}) + + assert listener.received == 1 + assert cid_to_change in listener.components_changed + + assert_array_equal(test_data['x'], new_data) diff --git a/glue/viewers/common/tests/test_viewer.py b/glue/viewers/common/tests/test_viewer.py index a7d6ad412..61f18de69 100644 --- a/glue/viewers/common/tests/test_viewer.py +++ b/glue/viewers/common/tests/test_viewer.py @@ -3,6 +3,7 @@ from glue.core import Data from glue.viewers.common.viewer import Viewer from glue.viewers.common.layer_artist import LayerArtist +from glue.core.message import NumericalDataChangedMessage def test_custom_layer_artist_maker(): @@ -45,3 +46,60 @@ def custom_maker(viewer, data): viewer.add_data(data2) assert len(viewer.layers) == 2 assert type(viewer.layers[1]) is CustomLayerArtist + + +def test_viewer_update_data(): + """ + Test that we can have a LayerArtist that can respond + to a NumericalDataChangedMessage. + """ + + class CustomUpdateLayerArtist(LayerArtist): + + def _on_components_changed(self, components_changed): + self.called_component_limits = True + self.num_components_changed = len(components_changed) + + class CustomApplication(Application): + def add_widget(self, *args, **kwargs): + pass + + @layer_artist_maker('custom_maker_for_update') + def custom_maker_for_update(viewer, data): + if hasattr(data, 'custom_for_update'): + return CustomUpdateLayerArtist(viewer.state, layer=data) + if hasattr(data.data, 'custom_for_update'): + return CustomUpdateLayerArtist(viewer.state, layer=data) + + app = CustomApplication() + + data1 = Data(x=[1, 2, 3], label='test1') + data1.custom_for_update = True + + app.data_collection.append(data1) + + viewer = app.new_data_viewer(Viewer) + + assert len(viewer.layers) == 0 + + # NOTE: Check exact type, not using isinstance + viewer.add_data(data1) + assert len(viewer.layers) == 1 + assert type(viewer.layers[0]) is CustomUpdateLayerArtist + + msg = NumericalDataChangedMessage(data1, components_changed=[data1.id['x']]) + + viewer._update_data(msg) + assert viewer.layers[0].called_component_limits + assert viewer.layers[0].num_components_changed == 1 + + subset = data1.new_subset() + subset.subset_state = data1.id['x'] > 2 + + assert len(viewer.layers) == 2 + assert type(viewer.layers[1]) is CustomUpdateLayerArtist + + msg = NumericalDataChangedMessage(data1, components_changed=[data1.id['x']]) + viewer._update_data(msg) + assert viewer.layers[1].called_component_limits + assert viewer.layers[1].num_components_changed == 1 diff --git a/glue/viewers/common/viewer.py b/glue/viewers/common/viewer.py index cc2aa4b57..71703c8d5 100644 --- a/glue/viewers/common/viewer.py +++ b/glue/viewers/common/viewer.py @@ -288,9 +288,20 @@ def _update_data(self, message): if isinstance(layer_artist.layer, Subset): if layer_artist.layer.data is message.data: layer_artist.update() + try: + components_changed = message.components_changed + layer_artist._on_components_changed(components_changed) + except AttributeError: + pass + else: if layer_artist.layer is message.data: layer_artist.update() + try: + components_changed = message.components_changed + layer_artist._on_components_changed(components_changed) + except AttributeError: + pass def _update_subset(self, message): if message.attribute == 'style':