2222from Orange .widgets .utils .itemmodels import DomainModel
2323from Orange .widgets .utils .widgetpreview import WidgetPreview
2424from Orange .widgets .utils .annotated_data import \
25- create_annotated_table , ANNOTATED_DATA_SIGNAL_NAME
26- from Orange .widgets .utils .colorpalette import ContinuousPaletteGenerator
25+ create_annotated_table , create_groups_table , ANNOTATED_DATA_SIGNAL_NAME
26+ from Orange .widgets .utils .colorpalette import \
27+ ContinuousPaletteGenerator , ColorPaletteGenerator
2728from Orange .widgets .visualize .utils import CanvasRectangle , CanvasText
2829from Orange .widgets .visualize .utils .plotutils import wrap_legend_items
2930
3233
3334
3435class SomView (QGraphicsView ):
35- SelectionClear , SelectionAdd , SelectionRemove , SelectionToggle = 1 , 2 , 4 , 8
36- SelectionSet = SelectionClear | SelectionAdd
37- selection_changed = Signal (set , int )
36+ SelectionSet , SelectionNewGroup , SelectionAddToGroup , SelectionRemove \
37+ = range ( 4 )
38+ selection_changed = Signal (np . ndarray , int )
3839 selection_moved = Signal (QKeyEvent )
39- selection_mark_changed = Signal (set )
40+ selection_mark_changed = Signal (np . ndarray )
4041
4142 def __init__ (self , scene ):
4243 super ().__init__ (scene )
@@ -56,32 +57,28 @@ def _get_marked_cells(self, event):
5657 x0 , x1 = sorted ((x0 , x1 ))
5758 y0 , y1 = sorted ((y0 , y1 ))
5859
60+ selection = np .zeros ((self .size_x , self .size_y ), dtype = bool )
5961 if self .hexagonal :
6062 y0 = max (0 , int (y0 / sqrt3_2 + 0.5 ))
6163 y1 = min (self .size_y , int (np .ceil (y1 / sqrt3_2 + 0.5 )))
62- selection = set ()
6364 for y in range (y0 , y1 ):
6465 x0_ = max (0 , int (x0 + 0.5 - (y % 2 ) / 2 ))
6566 x1_ = min (self .size_x - y % 2 ,
6667 int (np .ceil (x1 + 0.5 - (y % 2 ) / 2 )))
67- selection |= {(x , y ) for x in range (x0_ , x1_ )}
68- return selection
68+ selection [x0_ :x1_ , y ] = True
69+ elif not (x1 < - 0.5 or x0 > self .size_x - 0.5
70+ or y1 < - 0.5 or y0 > self .size_y - 0.5 ):
6971
70- else :
7172 def roundclip (z , zmax ):
7273 return int (np .clip (np .round (z ), 0 , zmax - 1 ))\
7374
74- if x1 < - 0.5 or x0 > self .size_x - 0.5 \
75- or y1 < - 0.5 or y0 > self .size_y - 0.5 :
76- return set ()
77-
7875 x0 = roundclip (x0 , self .size_x )
7976 y0 = roundclip (y0 , self .size_y )
8077 x1 = roundclip (x1 , self .size_x )
8178 y1 = roundclip (y1 , self .size_y )
82- return {( x , y )
83- for x in range ( x0 , x1 + 1 )
84- for y in range ( y0 , y1 + 1 )}
79+ selection [ x0 : x1 + 1 , y0 : y1 + 1 ] = True
80+
81+ return selection
8582
8683 def mousePressEvent (self , event ):
8784 if event .button () != Qt .LeftButton :
@@ -101,14 +98,15 @@ def mouseReleaseEvent(self, event):
10198 if event .button () != Qt .LeftButton :
10299 return
103100
104- if event .modifiers () & Qt .ControlModifier :
105- action = self .SelectionToggle
101+ if event .modifiers () & Qt .ShiftModifier :
102+ if event .modifiers () & Qt .ControlModifier :
103+ action = self .SelectionAddToGroup
104+ else :
105+ action = self .SelectionNewGroup
106106 elif event .modifiers () & Qt .AltModifier :
107107 action = self .SelectionRemove
108- elif event .modifiers () & Qt .ShiftModifier :
109- action = self .SelectionAdd
110108 else :
111- action = self .SelectionClear | self . SelectionAdd
109+ action = self .SelectionSet
112110 selection = self ._get_marked_cells (event )
113111 self .selection_changed .emit (selection , action )
114112 event .accept ()
@@ -151,7 +149,7 @@ def paint(self, painter, _option, _index):
151149
152150
153151class OWSOM (OWWidget ):
154- name = "Self-organizing Map"
152+ name = "Self-Organizing Map"
155153 description = "Computation of self-organizing map."
156154 icon = "icons/SOM.svg"
157155 keywords = ["SOM" ]
@@ -173,7 +171,7 @@ class Outputs:
173171 attr_color = ContextSetting (None )
174172 size_by_instances = Setting (True )
175173 pie_charts = Setting (False )
176- selection = Setting (set () , schema_only = True )
174+ selection = Setting (None , schema_only = True )
177175
178176 graph_name = "view"
179177
@@ -203,7 +201,7 @@ def __init__(self):
203201
204202 self .data = self .cont_x = None
205203 self .cells = self .member_data = None
206- self .selection = set ()
204+ self .selection = None
207205 self .colors = self .thresholds = None
208206
209207 box = gui .vBox (self .controlArea , box = "SOM" )
@@ -214,7 +212,7 @@ def __init__(self):
214212 box2 = gui .indentedBox (box , 10 )
215213 auto_dim = gui .checkBox (
216214 box2 , self , "auto_dimension" , "Set dimensions automatically" ,
217- callback = self .recompute_dimensions )
215+ callback = self .on_auto_dimension_changed )
218216 self .manual_box = box3 = gui .hBox (box2 )
219217 spinargs = dict (
220218 value = "" , widget = box3 , master = self , minv = 5 , maxv = 100 , step = 5 ,
@@ -225,11 +223,12 @@ def __init__(self):
225223 spin_y = gui .spin (** spinargs )
226224 spin_x .setValue (self .size_y )
227225 gui .rubber (box3 )
226+ self .manual_box .setEnabled (not self .auto_dimension )
228227
229228 initialization = gui .comboBox (
230229 box , self , "initialization" ,
231230 items = ("Initialize with PCA" , "Random initialization" ,
232- "Replicable random" ))
231+ "Replicable random" ))
233232
234233 start = gui .button (
235234 box , self , "Restart" , callback = self .restart_som_pressed ,
@@ -299,7 +298,7 @@ def set_data(self, data):
299298 else :
300299 if np .all (mask ):
301300 self .data = data
302- self .cont_x = x
301+ self .cont_x = x . copy ()
303302 else :
304303 self .data = data [mask ]
305304 self .cont_x = x [mask ]
@@ -350,15 +349,23 @@ def clear(self):
350349 self .Error .clear ()
351350
352351 def recompute_dimensions (self ):
352+ if not self .auto_dimension or self .cont_x is None :
353+ return
354+ dim = max (5 , int (np .ceil (np .sqrt (5 * np .sqrt (self .cont_x .shape [0 ])))))
355+ self .opt_controls .spin_x .setValue (dim )
356+ self .opt_controls .spin_y .setValue (dim )
357+
358+ def on_auto_dimension_changed (self ):
353359 self .manual_box .setEnabled (not self .auto_dimension )
354- if not self .auto_dimension and self .cont_x is not None :
355- dimx = dimy = \
356- max (5 , int (np .ceil (np .sqrt (5 * np .sqrt (self .cont_x .shape [0 ])))))
360+ if self .auto_dimension :
361+ self .recompute_dimensions ()
357362 else :
358- dimx = int (5 * np .round (self .size_x / 5 ))
359- dimy = int (5 * np .round (self .size_y / 5 ))
360- self .opt_controls .spin_x .setValue (dimx )
361- self .opt_controls .spin_y .setValue (dimy )
363+ spin_x = self .opt_controls .spin_x
364+ spin_y = self .opt_controls .spin_y
365+ dimx = int (5 * np .round (spin_x .value () / 5 ))
366+ dimy = int (5 * np .round (spin_y .value () / 5 ))
367+ spin_x .setValue (dimx )
368+ spin_y .setValue (dimy )
362369
363370 def on_attr_color_change (self ):
364371 self .controls .pie_charts .setEnabled (self .attr_color is not None )
@@ -374,33 +381,35 @@ def on_pie_chart_change(self):
374381 self ._redraw ()
375382
376383 def clear_selection (self ):
377- self .selection . clear ()
384+ self .selection = None
378385 self .redraw_selection ()
379386
380387 def on_selection_change (self , selection , action = SomView .SelectionSet ):
381- if action & SomView .SelectionClear :
382- self .selection .clear ()
383- if action & SomView .SelectionAdd :
384- self .selection |= selection
388+ if self .selection is None :
389+ self .selection = np .zeros (self .grid_cells .T .shape , dtype = np .int16 )
390+ if action == SomView .SelectionSet :
391+ self .selection [:] = 0
392+ self .selection [selection ] = 1
393+ elif action == SomView .SelectionAddToGroup :
394+ self .selection [selection ] = max (1 , np .max (self .selection ))
395+ elif action == SomView .SelectionNewGroup :
396+ self .selection [selection ] = 1 + np .max (self .selection )
385397 elif action & SomView .SelectionRemove :
386- self .selection -= selection
387- elif action & SomView .SelectionToggle :
388- self .selection ^= selection
398+ self .selection [selection ] = 0
389399 self .redraw_selection ()
390400 self .update_output ()
391401
392402 def on_selection_move (self , event : QKeyEvent ):
393- if len (self .selection ) > 1 :
394- return
395-
396- if not self .selection :
403+ if self .selection is None or not np .any (self .selection ):
397404 if event .key () in (Qt .Key_Right , Qt .Key_Down ):
398405 x = y = 0
399406 else :
400407 x = self .size_x - 1
401408 y = self .size_y - 1
402409 else :
403- x , y = next (iter (self .selection ))
410+ x , y = np .nonzero (self .selection )
411+ if len (x ) > 1 :
412+ return
404413 if event .key () == Qt .Key_Up and y > 0 :
405414 y -= 1
406415 if event .key () == Qt .Key_Down and y < self .size_y - 1 :
@@ -409,34 +418,45 @@ def on_selection_move(self, event: QKeyEvent):
409418 x -= 1
410419 if event .key () == Qt .Key_Right and x < self .size_x - 1 :
411420 x += 1
421+ x -= self .hexagonal and x == self .size_x - 1 and y % 2
412422
413- x -= self .hexagonal and x == self .size_x - 1 and y % 2
414- if {(x , y )} != self .selection :
415- self .on_selection_change ({(x , y )})
423+ if self .selection is not None and self .selection [x , y ]:
424+ return
425+ selection = np .zeros (self .grid_cells .shape , dtype = bool )
426+ selection [x , y ] = True
427+ self .on_selection_change (selection )
416428
417429 def on_selection_mark_change (self , marks ):
418430 self .redraw_selection (marks = marks )
419431
420432 def redraw_selection (self , marks = None ):
421433 if self .grid_cells is None :
422434 return
423- mark_brush = QBrush (QColor (224 , 255 , 255 ))
424- brushes = [[QBrush (Qt .NoBrush ), QBrush (QColor (240 , 240 , 255 ))],
425- [mark_brush , mark_brush ]]
435+
426436 sel_pen = QPen (QBrush (QColor (128 , 128 , 128 )), 2 )
427437 sel_pen .setCosmetic (True )
428438 mark_pen = QPen (QBrush (QColor (128 , 128 , 128 )), 4 )
429439 mark_pen .setCosmetic (True )
430- pens = [[self ._grid_pen , sel_pen ],
431- [mark_pen , mark_pen ]]
440+ pens = [self ._grid_pen , sel_pen ]
441+
442+ mark_brush = QBrush (QColor (224 , 255 , 255 ))
443+ sels = self .selection is not None and np .max (self .selection )
444+ palette = ColorPaletteGenerator (number_of_colors = sels + 1 )
445+ brushes = [QBrush (Qt .NoBrush )] + \
446+ [QBrush (palette [i ].lighter (165 )) for i in range (sels )]
447+
432448 for y in range (self .size_y ):
433449 for x in range (self .size_x - (y % 2 ) * self .hexagonal ):
434450 cell = self .grid_cells [y , x ]
435- selected = (x , y ) in self .selection
436- marked = bool (marks ) and (x , y ) in marks
437- cell .setBrush (brushes [marked ][selected ])
438- cell .setPen (pens [marked ][selected ])
439- cell .setZValue (marked or selected )
451+ marked = marks is not None and marks [x , y ]
452+ sel_group = self .selection is not None and self .selection [x , y ]
453+ if marked :
454+ cell .setBrush (mark_brush )
455+ cell .setPen (mark_pen )
456+ else :
457+ cell .setBrush (brushes [sel_group ])
458+ cell .setPen (pens [bool (sel_group )])
459+ cell .setZValue (marked or sel_group )
440460
441461 def restart_som_pressed (self ):
442462 if self ._optimizer_thread is not None :
@@ -445,11 +465,14 @@ def restart_som_pressed(self):
445465 self .start_som ()
446466
447467 def start_som (self ):
448- self .read_controls ()
468+ self .read_controls ()
469+ self .update_layout ()
470+ self .clear_selection ()
471+ if self .cont_x is not None :
449472 self .enable_controls (False )
450- self .update_layout ()
451- self .clear_selection ()
452473 self ._recompute_som ()
474+ else :
475+ self .update_output ()
453476
454477 def read_controls (self ):
455478 c = self .opt_controls
@@ -502,7 +525,7 @@ def _grid_factors(self):
502525
503526 def _draw_same_color (self , sizes ):
504527 fx , fy = self ._grid_factors
505- pen = QPen (QBrush (Qt .black ), 4 )
528+ pen = QPen (QBrush (Qt .black ), 2 )
506529 pen .setCosmetic (True )
507530 brush = QBrush (QColor (192 , 192 , 192 ))
508531 for y in range (self .size_y ):
@@ -566,8 +589,8 @@ def _draw_colored_circles(self, sizes):
566589 self .Warning .missing_colors (self .attr_color .name )
567590 bc = np .bincount (color_dist , minlength = len (self .colors ))
568591 color = self .colors [np .argmax (bc )]
569- pen = QPen (QBrush (color ), 4 )
570- brush = QBrush (color .lighter (200 - 100 * np .max (bc ) / len (members )))
592+ pen = QPen (QBrush (color ), 2 )
593+ brush = QBrush (color .lighter (200 - 80 * np .max (bc ) / len (members )))
571594 pen .setCosmetic (True )
572595 ellipse = QGraphicsEllipseItem ()
573596 ellipse .setRect (x + (y % 2 ) * fx - r / 2 , y * fy - r / 2 , r , r )
@@ -723,19 +746,33 @@ def rescale(self):
723746 self .size_y - 0.5 + leg_height / scale )
724747
725748 def update_output (self ):
726- indices = []
727- if self .data is not None :
728- for (x , y ) in self .selection :
729- indices .extend (self .get_member_indices (x , y ))
730- if indices :
731- self .Outputs .selected_data .send (self .data [indices ])
732- self .info .set_output_summary (str (len (indices )))
749+ if self .data is None :
750+ self .Outputs .selected_data .send (None )
751+ self .Outputs .annotated_data .send (None )
752+ self .info .set_output_summary (self .info .NoOutput )
753+ return
754+
755+ indices = np .zeros (len (self .data ), dtype = int )
756+ if self .selection is not None and np .any (self .selection ):
757+ for y in range (self .size_y ):
758+ for x in range (self .size_x ):
759+ rows = self .get_member_indices (x , y )
760+ indices [rows ] = self .selection [x , y ]
761+
762+ if np .any (indices ):
763+ sel_data = create_groups_table (self .data , indices , False , "Group" )
764+ self .Outputs .selected_data .send (sel_data )
765+ self .info .set_output_summary (str (len (sel_data )))
733766 else :
734767 self .Outputs .selected_data .send (None )
735768 self .info .set_output_summary (self .info .NoOutput )
736769
737- self .Outputs .annotated_data .send (
738- create_annotated_table (self .data , indices or None ))
770+ if np .max (indices ) > 1 :
771+ annotated = create_groups_table (self .data , indices )
772+ else :
773+ annotated = create_annotated_table (
774+ self .data , np .flatnonzero (indices ))
775+ self .Outputs .annotated_data .send (annotated )
739776
740777 def set_color_bins (self ):
741778 if self .attr_color is None :
@@ -818,6 +855,4 @@ def _draw_hexagon():
818855
819856
820857if __name__ == "__main__" : # pragma: no cover
821- WidgetPreview (OWSOM ).run (Table ("heart_disease" ))
822- # If run on sparse data, the widget core dumps if the user tries resizing it?!
823- # WidgetPreview(OWSOM).run(Table("/Users/janez/Downloads/deerwester.pkl"))
858+ WidgetPreview (OWSOM ).run (Table ("iris" ))
0 commit comments