@@ -128,7 +128,8 @@ def restoreAnchor(self, anchors):
128128 anchor , parentanchor = anchors
129129 self .anchor (* bound_anchor_pos (anchor , parentanchor ))
130130
131- def paint (self , painter , option , widget = None ):
131+ # pylint: disable=arguments-differ
132+ def paint (self , painter , _option , _widget = None ):
132133 painter .setPen (self .__pen )
133134 painter .setBrush (self .__brush )
134135 rect = self .contentsRect ()
@@ -244,22 +245,60 @@ def __init__(self, scatter_widget, parent=None, _="None", view_box=InteractiveVi
244245
245246
246247class ScatterPlotItem (pg .ScatterPlotItem ):
247- """PyQtGraph's ScatterPlotItem calls updateSpots at any change of sizes/colors/symbols,
248- which then rebuilds the stored pixmaps for each symbol. Because Orange calls
249- set* function in succession, we postpone updateSpots() to paint()."""
248+ """
249+ Modifies the behaviour of ScatterPlotItem as follows:
250+
251+ - Add z-index. ScatterPlotItem paints points in order of appearance in
252+ self.data. Plotting by z-index is achieved by sorting before calling
253+ super().paint() and re-sorting afterwards. Re-sorting (instead of
254+ storing the original data) is needed because the inherited paint
255+ may modify the data.
256+
257+ - Prevent multiple calls to updateSpots. ScatterPlotItem calls updateSpots
258+ at any change of sizes/colors/symbols, which then rebuilds the stored
259+ pixmaps for each symbol. Orange calls set* functions in succession,
260+ so we postpone updateSpots() to paint()."""
261+
262+ def __init__ (self , * args , ** kwargs ):
263+ super ().__init__ (* args , ** kwargs )
264+ self ._update_spots_in_paint = False
265+ self ._z_mapping = None
266+ self ._inv_mapping = None
267+
268+ def setZ (self , z ):
269+ """
270+ Set z values for all points.
271+
272+ Points with higher values are plotted on top of those with lower.
250273
251- _update_spots_in_paint = False
274+ Args:
275+ z (np.ndarray or None): a vector of z values
276+ """
277+ if z is None :
278+ self ._z_mapping = self ._inv_mapping = None
279+ else :
280+ assert len (z ) == len (self .data )
281+ self ._z_mapping = np .argsort (z )
282+ self ._inv_mapping = np .argsort (self ._z_mapping )
252283
253284 def updateSpots (self , dataSet = None ): # pylint: disable=unused-argument
254285 self ._update_spots_in_paint = True
255286 self .update ()
256287
288+ # pylint: disable=arguments-differ
257289 def paint (self , painter , option , widget = None ):
258- if self ._update_spots_in_paint :
259- self ._update_spots_in_paint = False
260- super ().updateSpots ()
261- painter .setRenderHint (QPainter .SmoothPixmapTransform , True )
262- super ().paint (painter , option , widget )
290+ try :
291+ if self ._z_mapping is not None :
292+ assert len (self ._z_mapping ) == len (self .data )
293+ self .data = self .data [self ._z_mapping ]
294+ if self ._update_spots_in_paint :
295+ self ._update_spots_in_paint = False
296+ super ().updateSpots ()
297+ painter .setRenderHint (QPainter .SmoothPixmapTransform , True )
298+ super ().paint (painter , option , widget )
299+ finally :
300+ if self ._inv_mapping is not None :
301+ self .data = self .data [self ._inv_mapping ]
263302
264303
265304def _define_symbols ():
@@ -581,7 +620,7 @@ def _get_jittering_tooltip(self):
581620 def update_jittering (self ):
582621 self .update_tooltip ()
583622 x , y = self .get_coordinates ()
584- if x is None or not len (x ) or self .scatterplot_item is None :
623+ if x is None or len (x ) == 0 or self .scatterplot_item is None :
585624 return
586625 self ._update_plot_coordinates (self .scatterplot_item , x , y )
587626 self ._update_plot_coordinates (self .scatterplot_item_sel , x , y )
@@ -766,7 +805,9 @@ def _jitter_data(self, x, y, span_x=None, span_y=None):
766805
767806 def _update_plot_coordinates (self , plot , x , y ):
768807 """
769- Change the coordinates of points while keeping other properites
808+ Change the coordinates of points while keeping other properites.
809+
810+ Asserts that the number of points stays the same.
770811
771812 Note. Pyqtgraph does not offer a method for this: setting coordinates
772813 invalidates other data. We therefore retrieve the data to set it
@@ -776,6 +817,7 @@ def _update_plot_coordinates(self, plot, x, y):
776817 update for every property would essentially reset the graph, which
777818 can be time consuming.
778819 """
820+ assert self .n_shown == len (x ) == len (y )
779821 data = dict (x = x , y = y )
780822 for prop in ('pen' , 'brush' , 'size' , 'symbol' , 'data' ,
781823 'sourceRect' , 'targetRect' ):
@@ -793,7 +835,7 @@ def update_coordinates(self):
793835 the complete update by calling `reset_graph` instead of this method.
794836 """
795837 x , y = self .get_coordinates ()
796- if x is None or not len (x ):
838+ if x is None or len (x ) == 0 :
797839 return
798840 if self .scatterplot_item is None :
799841 if self .sample_indices is None :
@@ -1012,9 +1054,9 @@ def _get_continuous_colors(self, c_data, subset):
10121054 # Reuse pens and brushes with the same colors because PyQtGraph then
10131055 # builds smaller pixmap atlas, which makes the drawing faster
10141056
1015- def reuse (cache , fn , * args ):
1057+ def reuse (cache , fun , * args ):
10161058 if args not in cache :
1017- cache [args ] = fn (args )
1059+ cache [args ] = fun (args )
10181060 return cache [args ]
10191061
10201062 def create_pen (col ):
@@ -1072,7 +1114,7 @@ def _get_discrete_colors(self, c_data, subset):
10721114
10731115 def update_colors (self ):
10741116 """
1075- Trigger an update of point sizes
1117+ Trigger an update of point colors
10761118
10771119 The method calls `self.get_colors`, which in turn calls the widget's
10781120 `get_color_data` to get the indices in the pallette. `get_colors`
@@ -1084,6 +1126,7 @@ def update_colors(self):
10841126 pen_data , brush_data = self .get_colors ()
10851127 self .scatterplot_item .setPen (pen_data , update = False , mask = None )
10861128 self .scatterplot_item .setBrush (brush_data , mask = None )
1129+ self .update_z_values ()
10871130 self .update_legends ()
10881131 self .update_density ()
10891132
@@ -1128,6 +1171,7 @@ def update_selection_colors(self):
11281171 pen , brush = self .get_colors_sel ()
11291172 self .scatterplot_item_sel .setPen (pen , update = False , mask = None )
11301173 self .scatterplot_item_sel .setBrush (brush , mask = None )
1174+ self .update_z_values ()
11311175
11321176 def get_colors_sel (self ):
11331177 """
@@ -1292,6 +1336,52 @@ def update_shapes(self):
12921336 self .scatterplot_item .setSymbol (shape_data )
12931337 self .update_legends ()
12941338
1339+ def update_z_values (self ):
1340+ """
1341+ Set z-values for point in the plot
1342+
1343+ The order is as follows:
1344+ - selected points that are also in the subset on top,
1345+ - followed by selected points,
1346+ - followed by points from the subset,
1347+ - followed by the rest.
1348+ Within each of these four groups, points are ordered by their colors.
1349+
1350+ Points with less frequent colors are above those with more frequent.
1351+ The points for which the value for the color is missing are at the
1352+ bottom of their respective group.
1353+ """
1354+ if not self .scatterplot_item :
1355+ return
1356+
1357+ subset = self .master .get_subset_mask ()
1358+ c_data = self .master .get_color_data ()
1359+ if subset is None and self .selection is None and c_data is None :
1360+ self .scatterplot_item .setZ (None )
1361+ return
1362+
1363+ z = np .zeros (self .n_shown )
1364+
1365+ if subset is not None :
1366+ subset = self ._filter_visible (subset )
1367+ z [subset ] += 1000
1368+
1369+ if self .selection is not None :
1370+ z [self ._filter_visible (self .selection ) != 0 ] += 2000
1371+
1372+ if c_data is not None :
1373+ c_nan = np .isnan (c_data )
1374+ vis_data = self ._filter_visible (c_data )
1375+ vis_nan = np .isnan (vis_data )
1376+ z [vis_nan ] -= 999
1377+ if not self .master .is_continuous_color ():
1378+ dist = np .bincount (c_data [~ c_nan ].astype (int ))
1379+ vis_knowns = vis_data [~ vis_nan ].astype (int )
1380+ argdist = np .argsort (dist )
1381+ z [~ vis_nan ] -= argdist [vis_knowns ]
1382+
1383+ self .scatterplot_item .setZ (z )
1384+
12951385 def update_grid_visibility (self ):
12961386 """Show or hide the grid"""
12971387 self .plot_widget .showGrid (x = self .show_grid , y = self .show_grid )
0 commit comments