@@ -230,6 +230,24 @@ def update_coordinates(self):
230230 self .view_box .setAspectLocked (True , 1 )
231231
232232
233+ class invalidated :
234+ pca_projection = affinities = tsne_embedding = False
235+
236+ def __set__ (self , instance , value ):
237+ # `self._invalidate = True` should invalidate everything
238+ self .pca_projection = self .affinities = self .tsne_embedding = value
239+
240+ def __bool__ (self ):
241+ # If any of the values are invalidated, this should return true
242+ return self .pca_projection or self .affinities or self .tsne_embedding
243+
244+ def __str__ (self ):
245+ return "%s(%s)" % (self .__class__ .__name__ , ", " .join (
246+ "=" .join ([k , str (getattr (self , k ))])
247+ for k in ["pca_projection" , "affinities" , "tsne_embedding" ]
248+ ))
249+
250+
233251class OWtSNE (OWDataProjectionWidget , ConcurrentWidgetMixin ):
234252 name = "t-SNE"
235253 description = "Two-dimensional data projection with t-SNE."
@@ -250,6 +268,11 @@ class OWtSNE(OWDataProjectionWidget, ConcurrentWidgetMixin):
250268
251269 left_side_scrolling = True
252270
271+ # Use `invalidated` descriptor so we don't break the usage of
272+ # `_invalidated` in `OWDataProjectionWidget`, but still allow finer control
273+ # over which parts of the embedding to invalidate
274+ _invalidated = invalidated ()
275+
253276 class Information (OWDataProjectionWidget .Information ):
254277 modified = Msg ("The parameter settings have been changed. Press "
255278 "\" Start\" to rerun with the new settings." )
@@ -323,21 +346,19 @@ def _multiscale_changed(self):
323346 self ._invalidate_affinities ()
324347
325348 def _invalidate_pca_projection (self ):
326- self .pca_projection = None
327- self .initialization = None
349+ self ._invalidated .pca_projection = True
328350 self ._invalidate_affinities ()
329351
330352 def _invalidate_affinities (self ):
331- self .affinities = None
353+ self ._invalidated . affinities = True
332354 self ._invalidate_tsne_embedding ()
333355
334356 def _invalidate_tsne_embedding (self ):
335- self .iterations_done = 0
336- self .tsne_embedding = None
337- self ._invalidate_output ()
357+ self ._invalidated .tsne_embedding = True
358+ self ._stop_running_task ()
338359 self ._set_modified (True )
339360
340- def _invalidate_output (self ):
361+ def _stop_running_task (self ):
341362 self .cancel ()
342363 self .run_button .setText ("Start" )
343364
@@ -397,8 +418,15 @@ def _toggle_run(self):
397418 else :
398419 self .run ()
399420
400- def set_data (self , data : Table ):
401- super ().set_data (data )
421+ def handleNewSignals (self ):
422+ # We don't bother with the granular invalidation flags because
423+ # `super().handleNewSignals` will just set all of them to False or will
424+ # do nothing. However, it's important we remember its state because we
425+ # won't call `run` if needed. `run` also relies on the state of
426+ # `_invalidated` to properly set the intermediate values to None
427+ prev_invalidated = bool (self ._invalidated )
428+ super ().handleNewSignals ()
429+ self ._invalidated = prev_invalidated
402430
403431 if self ._invalidated :
404432 self .run ()
@@ -439,7 +467,17 @@ def enable_controls(self):
439467 self .controls .perplexity .setDisabled (self .multiscale )
440468
441469 def run (self ):
470+ # Reset invalidated values as indicated by the flags
471+ if self ._invalidated .pca_projection :
472+ self .pca_projection = None
473+ if self ._invalidated .affinities :
474+ self .affinities = None
475+ if self ._invalidated .tsne_embedding :
476+ self .iterations_done = 0
477+ self .tsne_embedding = None
478+
442479 self ._set_modified (False )
480+ self ._invalidated = False
443481
444482 # When the data is invalid, it is set to `None` and an error is set,
445483 # therefore it would be erroneous to clear the error here
0 commit comments