1 """
2 Classes providing GUI windows for PlotGroups, allowing sets of related plots
3 to be displayed.
4
5 $Id: plotgrouppanel.py 11310 2010-07-27 16:56:14Z ceball $
6 """
7 __version__='$Revision: 11310 $'
8
9
10 import copy
11
12 import ImageTk
13
14 import Tkinter
15 from Tkinter import Frame, TOP, YES, BOTH, X, LEFT, \
16 RIGHT, DISABLED, NORMAL, Canvas, Label, NSEW, \
17 NO, NONE,TclError
18
19 import param
20
21 from param import tk
22
23 from topo.base.sheet import Sheet
24 from topo.base.cf import CFSheet
25
26 from topo.command.pylabplot import matrixplot
27
28 from topo.misc.generatorsheet import GeneratorSheet
29
30 import topo
33 """
34 Decorator to show busy cursor for duration of fn call.
35 """
36 def busy_fn(widget,*args,**kw):
37 if 'cursor' in widget.configure():
38 old_cursor=widget['cursor']
39 widget['cursor']='watch'
40 widget.update_idletasks()
41
42 try:
43 fn(widget,*args,**kw)
44 finally:
45
46
47 if 'cursor' in widget.configure():
48 widget['cursor']=old_cursor
49
50 return busy_fn
51
54
55 __abstract = True
56
57
58 dock = param.Boolean(default=False,doc="on console or not")
59
60
61 button_image_size=(20,20)
62
63 Refresh = tk.Button(image_path="tkgui/icons/redo-small.png",
64 size=button_image_size,
65 doc="""
66 Refresh the current plot (i.e. force the current plot to be regenerated
67 by executing pre_plot_hooks and plot_hooks).""")
68
69 Redraw = tk.Button(image_path="tkgui/icons/redo-small.png",
70 size=button_image_size,
71 doc="""Redraw the plot from existing data (i.e. execute plot_hooks only).""")
72
73 Enlarge = tk.Button(image_path="tkgui/icons/viewmag+_2.2.png",
74 size=button_image_size,
75 doc="""Increase the displayed size of the current plots by about 20%.""")
76
77 Reduce = tk.Button(image_path="tkgui/icons/viewmag-_2.1.png",
78 size=button_image_size,doc="""
79 Reduce the displayed size of the current plots by about 20%.
80 A minimum size that preserves at least one pixel per unit is
81 enforced, to ensure that no data is lost when displaying.""")
82
83 Fwd = tk.Button(image_path="tkgui/icons/forward-2.0.png",
84 size=button_image_size,doc="""
85 Move forward through the history of all the plots shown in this window.""")
86
87 Back = tk.Button(image_path="tkgui/icons/back-2.0.png",
88 size=button_image_size,doc="""
89 Move backward through the history of all the plots shown in
90 this window. When showing a historical plot, some functions
91 will be disabled, because the original data is no longer
92 available.""")
93
94 gui_desired_maximum_plot_height = param.Integer(default=150,bounds=(0,None),doc="""
95 Value to provide for PlotGroup.desired_maximum_plot_height for
96 PlotGroups opened by the GUI. Determines the initial, default
97 scaling for the PlotGroup.""")
98
99
104
105 plotgroup = property(get_plotgroup,set_plotgroup)
106
107
108 - def __init__(self,master,plotgroup,**params):
109 """
110 If your parameter should be available in history, add its name
111 to the params_in_history list, otherwise it will be disabled
112 in historical views.
113 """
114
115 tk.TkParameterized.__init__(self,master,extraPO=plotgroup,
116 msg_handler=master.status,
117 **params)
118 Frame.__init__(self,master.content)
119
120 self.parent = master
121 self.setup_plotgroup()
122
123
124
125
126
127 self.canvases = []
128 self.plot_labels = []
129
130
131 self._num_labels = 0
132
133 self.plotgroups_history=[]
134 self.history_index = 0
135 self.params_in_history = []
136
137
138 self.zoom_factor = 1.2
139
140
141 self.control_frame_1 = Frame(master.noscroll)
142 self.control_frame_1.pack(side=TOP,expand=NO,fill=X)
143
144 self.control_frame_2 = Frame(master.noscroll)
145 self.control_frame_2.pack(side=TOP,expand=NO,fill=X)
146
147 self.plot_frame = Tkinter.LabelFrame(self,text=self.plotgroup.name)
148 self.plot_frame.pack(side=TOP,expand=YES,fill=BOTH)
149
150
151
152 self.plot_container = Tkinter.Frame(self.plot_frame)
153 self.plot_container.pack(anchor="center")
154
155
156
157
158
159 no_plot_note_text = """
160 Press Refresh on the pre-plot hooks to generate the plot, after modifying the hooks below if necessary. Note that Refreshing may take some time.
161
162 Many hooks accept 'display=True' so that the progress can be viewed in an open Activity window, e.g. for debugging.
163 """
164
165 self.no_plot_note=Label(self.plot_container,text=no_plot_note_text,
166 justify="center",wraplength=350)
167 self.no_plot_note_enabled=False
168
169
170 self.control_frame_3 = Frame(master.noscroll_bottom)
171 self.control_frame_3.pack(side=TOP,expand=NO,fill=X)
172
173 self.control_frame_4 = Frame(self)
174 self.control_frame_4.pack(side=TOP,expand=NO,fill=NONE)
175
176 self.updatecommand_frame = Frame(self.control_frame_3)
177 self.updatecommand_frame.pack(side=TOP,expand=YES,fill=X)
178
179 self.plotcommand_frame = Frame(self.control_frame_3)
180 self.plotcommand_frame.pack(side=TOP,expand=YES,fill=X)
181
182
183
184 self.messageBar = self.parent.status
185
186 self.pack_param('pre_plot_hooks',parent=self.updatecommand_frame,
187 expand='yes',fill='x',side='left')
188
189 self.pack_param('Refresh',parent=self.updatecommand_frame,
190 on_set=self.refresh,side='right')
191 self.params_in_history.append('Refresh')
192
193 self.pack_param('plot_hooks',parent=self.plotcommand_frame,
194 expand='yes',fill='x',side='left')
195
196 self.pack_param('Redraw',parent=self.plotcommand_frame,
197 on_set=self.redraw_plots,side='right')
198
199
200 self.pack_param('Enlarge',parent=self.control_frame_1,
201 on_set=self.enlarge_plots,side=LEFT)
202 self.params_in_history.append('Enlarge')
203
204 self.pack_param('Reduce',parent=self.control_frame_1,
205 on_set=self.reduce_plots,side=LEFT)
206 self.params_in_history.append('Reduce')
207
208
209 if topo.tkgui.TK_SUPPORTS_DOCK:
210 self.pack_param("dock",parent=self.control_frame_1,
211 on_set=self.set_dock,side=LEFT)
212
213
214
215
216
217 self.pack_param('Back',parent=self.control_frame_2,
218 on_set=lambda x=-1: self.navigate_pg_history(x),
219 side=LEFT)
220
221 self.pack_param('Fwd',parent=self.control_frame_2,
222 on_set=lambda x=+1: self.navigate_pg_history(x),
223 side=LEFT)
224
225
226
227
228
229
230
231 self._canvas_menu = tk.Menu(self, tearoff=0)
232
233 self._unit_menu = tk.Menu(self._canvas_menu, tearoff=0)
234 self._canvas_menu.add_cascade(menu=self._unit_menu,state=DISABLED,
235 indexname='unit_menu')
236
237 self._canvas_menu.add_separator()
238
239
240
241
242
243
244
245
246
247 self._unit_menu_updaters = {}
248
249 self._sheet_menu = tk.Menu(self._canvas_menu, tearoff=0)
250 self._canvas_menu.add_cascade(menu=self._sheet_menu,state=DISABLED,
251 indexname='sheet_menu')
252 self._canvas_menu.add_separator()
253
254
255 self.update_plot_frame(plots=False)
256
257
258
259
260
261
262
270
271
272
280
281
283 """
284 Return a dictionary containing the event itself, and, if the
285 event occurs on a plot of a sheet, store the plot and the
286 coordinates ((r,c) and (x,y) for the cell center) on the sheet.
287
288 Then, call func.
289 """
290
291
292
293 plot=event.widget.plot
294 event_info = {'event':event}
295
296
297
298 if plot.plot_src_name is not '':
299 plot_width,plot_height=plot.bitmap.width(),plot.bitmap.height()
300 if 0<=event.x<plot_width and 0<=event.y<plot_height:
301 left,bottom,right,top=plot.plot_bounding_box.lbrt()
302
303 x = (right-left)*float(event.x)/plot_width + left
304 y = top - (top-bottom)*float(event.y)/plot_height
305 r,c = topo.sim[plot.plot_src_name].sheet2matrixidx(x,y)
306 event_info['plot'] = plot
307 event_info['coords'] = [(r,c),(x,y)]
308
309 func(event_info)
310
311
313 """
314 Update labels on right-click menu and popup the menu, plus store the event info
315 for access by any menu commands that require it.
316
317 If show_menu is False, popup menu is not displayed (in case subclasses
318 wish to add extra menu items first).
319 """
320 if 'plot' in event_info:
321 plot = event_info['plot']
322
323 self._canvas_menu.entryconfig("sheet_menu",
324 label="Combined plot: %s %s"%(plot.plot_src_name,plot.name),
325 state=NORMAL)
326 (r,c),(x,y) = event_info['coords']
327 sheet = topo.sim[plot.plot_src_name]
328 self._canvas_menu.entryconfig("unit_menu",
329 label="Single unit:(% 3d,% 3d) Coord:(% 2.2f,% 2.2f)"%(r,c,x,y),
330 state=NORMAL)
331 self._right_click_info = event_info
332
333
334 for v in self._unit_menu_updaters.values(): v(plot)
335
336 if show_menu:
337 self._canvas_menu.tk_popup(event_info['event'].x_root,
338 event_info['event'].y_root)
339
340
341
342
344 """
345 Update dynamic information.
346 """
347 if 'plot' in event_info:
348 plot = event_info['plot']
349 (r,c),(x,y) = event_info['coords']
350 location_string="%s Unit:(% 3d,% 3d) Coord:(% 2.2f,% 2.2f)"%(plot.plot_src_name,r,c,x,y)
351
352 self.messageBar.dynamicinfo(self._dynamic_info_string(event_info,location_string))
353 else:
354 self.messageBar.dynamicinfo('')
355
356
357
358
360 """
361 Subclasses can override to add extra relevant information.
362 """
363 return x
364
365
366
367
369 """
370
371 set geom True for any action that user would expect to lose
372 his/her manual window size (e.g. pressing enlarge button)
373 """
374
375 if plots:
376 self.plotgroup.scale_images()
377 self.display_plots()
378 if labels: self.display_labels()
379 self.refresh_title()
380
381 if len(self.canvases)==0:
382
383 self.no_plot_note.grid(row=1,column=0,sticky='nsew')
384 self.no_plot_note_enabled=True
385 self.representations['Enlarge']['widget']['state']=DISABLED
386 self.representations['Reduce' ]['widget']['state']=DISABLED
387
388 elif self.no_plot_note_enabled:
389 self.no_plot_note.grid_forget()
390 self.no_plot_note_enabled=False
391 self.representations['Enlarge']['widget']['state']=NORMAL
392 self.representations['Reduce' ]['widget']['state']=NORMAL
393
394 self.__update_widgets_for_history()
395
396
397
398
399
400
401
402
403 self.parent.sizeright()
404 if geom:
405 try:
406 self.parent.geometry('')
407 except TclError:
408 pass
409
410
411 @with_busy_cursor
420
421
422 @with_busy_cursor
424 """
425 Call plotgroup's make_plots with update=False (i.e. run only
426 plot_hooks, not pre_plot_hooks), then display the result.
427 """
428 self.plotgroup.make_plots(update=False)
429 self.update_plot_frame(labels=False)
430
431
433 """
434 Rescale the existing plots, without calling either the
435 plot_hooks or the pre_plot_hooks, then display the result.
436 """
437 self.plotgroup.scale_images()
438 self.update_plot_frame(labels=False,geom=True)
439
440
442 """
443 Main steps for generating plots in the Frame.
444
445 # if update is True, the SheetViews are re-generated
446 """
447
448
449
450 if self.history_index!=0:
451 self._switch_plotgroup(copy.copy(self.plotgroups_history[-1]))
452 self.history_index = 0
453
454 if update:
455 self.refresh_plots()
456 else:
457 self.redraw_plots()
458
459
460
461
462
464 """Calculate self._rows and self._cols, together giving the grid position of each plot."""
465 distinct_precedences = sorted(set([p.row_precedence for p in plots]))
466
467
468 precedence2row = dict([ (precedence,2*i)
469 for precedence,i in zip(distinct_precedences,
470 range(len(distinct_precedences)))])
471
472 self._rows = [precedence2row[p.row_precedence] for p in plots]
473 self._cols = []
474
475 row_counts = dict([(row,0) for row in self._rows])
476 for row in self._rows:
477 self._cols.append(row_counts[row])
478 row_counts[row]+=1
479
480
481
483 """
484
485 This function should be redefined in subclasses for interesting
486 things such as 2D grids.
487 """
488 plots = self.plotgroup.plots
489 self._determine_layout_of_plots(plots)
490
491 self.zoomed_images = [ImageTk.PhotoImage(p.bitmap.image) for p in plots]
492
493 new_sizes = [(str(zi.width()),
494 str(zi.height()))
495 for zi in self.zoomed_images]
496 old_sizes = [(canvas['width'],canvas['height'])
497 for canvas in self.canvases]
498
499
500
501
502
503
504
505 if len(self.zoomed_images) != len(self.canvases) or \
506 new_sizes != old_sizes:
507
508 old_canvases = self.canvases
509 self.canvases = [Canvas(self.plot_container,
510 width=image.width(),
511 height=image.height(),
512 borderwidth=1,highlightthickness=0,
513 relief='groove')
514 for image in self.zoomed_images]
515 for i,image,canvas in zip(range(len(self.zoomed_images)),
516 self.zoomed_images,self.canvases):
517 canvas.create_image(1,1,anchor="nw",image=image)
518 canvas.grid(row=self._rows[i],column=self._cols[i],padx=5)
519
520
521 for c in old_canvases:
522 c.grid_forget()
523
524
525 else:
526
527 for i,image,canvas in zip(range(len(self.zoomed_images)),
528 self.zoomed_images,self.canvases):
529 canvas.create_image(1,1,anchor="nw",image=image)
530 canvas.grid(row=self._rows[i],column=self._cols[i],padx=5)
531
532 self._add_canvas_bindings()
533
534
536
537 for plot,canvas in zip(self.plotgroup.plots,self.canvases):
538
539
540
541 canvas.plot=plot
542
543
544
545
546
547 canvas.bind('<<right-click>>',lambda event: \
548 self.__process_canvas_event(event,self._canvas_right_click))
549 canvas.bind('<Motion>',lambda event: \
550 self.__process_canvas_event(event,self._update_dynamic_info))
551
552 canvas.bind('<Leave>',lambda event: \
553 self.__process_canvas_event(event,self._update_dynamic_info))
554
555
556
557
558 canvas.bind('<Button-1>',lambda event: \
559 self.__process_canvas_event(event,self._update_dynamic_info))
560
561
562
563
564
565
567 """
568 This function should be redefined by subclasses to match any
569 changes made to display__plots(). Depending on the situation,
570 it may be useful to make this function a stub, and display the
571 labels at the same time the images are displayed.
572 """
573
574 if len(self.canvases) == 0:
575 pass
576 elif self._num_labels != len(self.canvases):
577 old_labels = self.plot_labels
578 self.plot_labels = [Label(self.plot_container,text=each)
579 for each in self.plotgroup.labels]
580 for i in range(len(self.plot_labels)):
581 self.plot_labels[i].grid(row=self._rows[i]+1,column=self._cols[i],sticky=NSEW)
582 for l in old_labels:
583 l.grid_forget()
584 self._num_labels = len(self.canvases)
585 else:
586 for i in range(len(self.plot_labels)):
587 self.plot_labels[i].configure(text=self.plotgroup.labels[i])
588
589
590
591
592
593
594
595
597 """Function called by widget to reduce the plot size, when possible."""
598 if (not self.plotgroup.scale_images(1.0/self.zoom_factor)):
599 self.representations['Reduce']['widget']['state']=DISABLED
600 self.representations['Enlarge']['widget']['state']=NORMAL
601 self.update_plot_frame(labels=False,geom=True)
602
604 """Function called by widget to increase the plot size, when possible."""
605 if (not self.plotgroup.scale_images(self.zoom_factor)):
606 self.representations['Enlarge']['widget']['state']=DISABLED
607 self.representations['Reduce']['widget']['state']=NORMAL
608 self.update_plot_frame(labels=False,geom=True)
609
610
611
612
613
614
615
616
617
618
619
620
622 """
623 If there are plots on display, and we're not doing a history research,
624 the plotgroup is stored in the history.
625 """
626 if self.history_index==0 and not len(self.canvases)==0:
627 self.plotgroups_history.append(copy.copy(self.plotgroup))
628 self.__update_widgets_for_history()
629
655
656
658 """
659 The plotgroup's non-history widgets are all irrelevant when the plotgroup's from
660 history.
661 """
662 if self.history_index!=0:
663 state= 'disabled'
664 else:
665 state = 'normal'
666
667 widgets_to_update = [self.representations[p_name]['widget']
668 for p_name in self.representations
669 if p_name not in self.params_in_history]
670
671 for widget in widgets_to_update:
672 self.__set_widget_state(widget,state)
673
674 self.__update_history_buttons()
675
676
678 """
679 Enable/disable the back and forward buttons depending on
680 where we are in a history research.
681 """
682 space_back = len(self.plotgroups_history)+self.history_index-1
683 space_fwd = -self.history_index
684
685 back_button = self.representations['Back']['widget']
686 forward_button = self.representations['Fwd']['widget']
687
688 if space_back>0:
689 back_button['state']='normal'
690 else:
691 back_button['state']='disabled'
692
693 if space_fwd>0:
694 forward_button['state']='normal'
695 else:
696 forward_button['state']='disabled'
697
698
699
700
701 - def navigate_pg_history(self,steps):
702 self.history_index+=steps
703 self._switch_plotgroup(self.plotgroups_history[len(self.plotgroups_history)+self.history_index-1])
704 self.update_plot_frame()
705
706
707
708
722
723
724
725
726
727
729 """
730 Provide a string describing the current set of plots.
731
732 Override in subclasses to provide more information.
733 """
734 return "%s at time %s"%(self.plotgroup.name,topo.sim.timestr(self.plotgroup.time))
735
736
737
746
747
749 """overrides toplevel destroy, adding removal from autorefresh panels"""
750 if self in topo.guimain.auto_refresh_panels:
751 topo.guimain.auto_refresh_panels.remove(self)
752 Frame.destroy(self)
753
757
758 sheet_type = Sheet
759
760 @classmethod
761 - def valid_context(cls):
762 """
763 Return true if there appears to be data available for this type of plot.
764
765 To avoid confusing error messages, this method should be
766 defined to return False in the case where there is no
767 appropriate data to plot. This information can be used to,
768 e.g., gray out the appropriate menu item.
769 By default, PlotPanels are assumed to be valid only for
770 simulations that contain at least one Sheet. Subclasses with
771 more specific requirements should override this method with
772 something more appropriate.
773 """
774 if topo.sim.objects(cls.sheet_type).items():
775 return True
776 else:
777 return False
778
779
780 - def __init__(self,master,plotgroup,**params):
781 super(SheetPanel,self).__init__(master,plotgroup,**params)
782
783 self.pack_param('auto_refresh',parent=self.control_frame_1,
784 on_set=self.set_auto_refresh,
785 side=RIGHT)
786 self.params_in_history.append('auto_refresh')
787
788 if self.auto_refresh:
789 topo.guimain.auto_refresh_panels.append(self)
790
791
792 self.pack_param('normalize',parent=self.control_frame_1,
793 on_set=self.redraw_plots,side="right")
794 self.pack_param('integer_scaling',parent=self.control_frame_2,
795 on_set=self.rescale_plots,side='right')
796 self.pack_param('sheet_coords',parent=self.control_frame_2,
797 on_set=self.rescale_plots,side='right')
798
799 self.params_in_history.append('sheet_coords')
800 self.params_in_history.append('integer_scaling')
801
802
803
804 self._unit_menu.add_command(label='Connection Fields',indexname='connection_fields',
805 command=self._connection_fields_window)
806
807 self._unit_menu.add_command(label='Receptive Field',
808 indexname='receptive_field',
809 command=self._receptive_field_window)
810
811 self._unit_menu.add_command(label='Orientation Tuning Curves',
812 indexname='or tuning curve',
813 command=self._or_tuning_curve_window)
814
815
816 self._unit_menu_updaters['connection_fields'] = self.check_for_cfs
817 self._unit_menu_updaters['receptive_field'] = self.check_for_rfs
818
825
839
846
847
848
850 """
851 Add or remove this panel from the console's
852 auto_refresh_panels list.
853 """
854 if self.auto_refresh:
855 if not (self in topo.guimain.auto_refresh_panels):
856 topo.guimain.auto_refresh_panels.append(self)
857 else:
858 if self in topo.guimain.auto_refresh_panels:
859 topo.guimain.auto_refresh_panels.remove(self)
860
861
862
864 """
865 Open a Connection Fields plot for the unit currently
866 identified by a right click.
867 """
868 if 'plot' in self._right_click_info:
869 sheet = topo.sim[self._right_click_info['plot'].plot_src_name]
870
871 center_x,center_y = self._right_click_info['coords'][1]
872 topo.guimain['Plots']["Connection Fields"](x=center_x,y=center_y,sheet=sheet)
873
874
876 """
877 Open a Receptive Field plot for the unit currently
878 identified by a right click.
879 """
880 if 'plot' in self._right_click_info:
881 plot = self._right_click_info['plot']
882 sheet = topo.sim[plot.plot_src_name]
883 center_x,center_y=self._right_click_info['coords'][1]
884 r,c = self._right_click_info['coords'][0]
885
886
887
888 for g in topo.sim.objects(GeneratorSheet).values():
889 try:
890 view=g.sheet_views[('RFs',sheet.name,center_x,center_y)]
891 matrixplot(view.view()[0],
892 title=("Receptive Field of %s unit (%d,%d) at coord (%3.0f, %3.0f) at time %s" %
893 (sheet.name,r,c,center_x,center_y,topo.sim.timestr(view.timestamp))))
894
895 except KeyError:
896
897 topo.sim.warning("No RF measurements are available yet for input_sheet %s; run the Receptive Field plot for that input_sheet to see the RF."%g.name)
898
899
901 """
902 Open a Tuning Curve plot for the unit currently
903 identified by a right click.
904 """
905 if 'plot' in self._right_click_info:
906 plot = self._right_click_info['plot']
907 sheet = topo.sim[plot.plot_src_name]
908 center_x,center_y=self._right_click_info['coords'][1]
909 r,c = self._right_click_info['coords'][0]
910
911 try:
912 from topo.command.pylabplot import cyclic_tuning_curve
913 cyclic_tuning_curve(x_axis="orientation",coords=[(center_x,center_y)],sheet=sheet)
914 except AttributeError:
915 topo.sim.warning("No orientation tuning curve measurements are available yet for sheet %s; run the Orientation Tuning (Fullfield) command and try again."%sheet.name)
916
917
919 """
920 Only calls refresh() if auto_refresh is enabled.
921 """
922 if self.auto_refresh:self.refresh()
923
929