source: trunk/python/asaplot.py@ 193

Last change on this file since 193 was 187, checked in by mar637, 20 years ago

Bug fix: Handling destruction of window via self._is_dead

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 14.6 KB
Line 
1"""
2ASAP plotting class based on matplotlib.
3"""
4
5import sys
6from re import match
7import Tkinter as Tk
8
9print "Importing matplotlib with TkAgg backend."
10import matplotlib
11matplotlib.use("TkAgg")
12
13from matplotlib.backends import new_figure_manager, show
14from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, \
15 FigureManagerTkAgg
16from matplotlib.figure import Figure, Text
17from matplotlib.numerix import sqrt
18
19# Force use of the newfangled toolbar.
20matplotlib.rcParams['toolbar'] = 'toolbar2'
21
22# Colour dictionary.
23colours = {}
24
25class ASAPlot:
26 """
27 ASAP plotting class based on matplotlib.
28 """
29
30 def __init__(self, rows=1, cols=0, title='', size=(8,6), buffering=False):
31 """
32 Create a new instance of the ASAPlot plotting class.
33
34 If rows < 1 then a separate call to set_panels() is required to define
35 the panel layout; refer to the doctext for set_panels().
36 """
37 self.window = Tk.Tk()
38 self.is_dead = False
39 def dest_callback():
40 self.is_dead = True
41 self.window.destroy()
42
43 self.window.protocol("WM_DELETE_WINDOW", dest_callback)
44 self.frame1 = Tk.Frame(self.window, relief=Tk.RIDGE, borderwidth=4)
45 self.frame1.pack(fill=Tk.BOTH)
46
47 self.figure = Figure(figsize=size, facecolor='#ddddee')
48 self.canvas = FigureCanvasTkAgg(self.figure, master=self.frame1)
49 self.canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
50
51 # Simply instantiating this is enough to get a working toolbar.
52 self.figmgr = FigureManagerTkAgg(self.canvas, 1, self.window)
53 self.window.wm_title('ASAPlot graphics window')
54
55 self.events = {'button_press':None,
56 'button_release':None,
57 'motion_notify':None}
58
59 self.set_title(title)
60 self.subplots = []
61 if rows > 0:
62 self.set_panels(rows, cols)
63
64
65 # Set matplotlib default colour sequence.
66 self.colours = [1, 'b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']
67 self.attributes = {}
68 self.loc = 1
69
70 matplotlib.interactive = True
71 self.buffering = buffering
72
73 self.canvas.show()
74
75
76 def clear(self):
77 """
78 Delete all lines from the plot. Line numbering will restart from 1.
79 """
80
81 for i in range(1,len(self.lines)+1):
82 self.delete(i)
83
84 self.axes.clear()
85 self.colours[0] = 1
86 self.lines = []
87
88
89 def delete(self, numbers=None):
90 """
91 Delete the 0-relative line number, default is to delete the last.
92 The remaining lines are NOT renumbered.
93 """
94
95 if numbers is None: numbers = [len(self.lines)-1]
96
97 if not hasattr(numbers, '__iter__'):
98 numbers = [numbers]
99
100 for number in numbers:
101 if 0 <= number < len(self.lines):
102 if self.lines[number] is not None:
103 for line in self.lines[number]:
104 line.set_linestyle('None')
105 self.lines[number] = None
106
107 self.show()
108
109
110 def get_line(self):
111 """
112 Get the current default line attributes.
113 """
114 return self.attributes
115
116
117 def hist(self, x=None, y=None, fmt=None):
118 """
119 Plot a histogram. N.B. the x values refer to the start of the
120 histogram bin.
121
122 fmt is the line style as in plot().
123 """
124
125 if x is None:
126 if y is None: return
127 x = range(0,len(y))
128
129 if len(x) != len(y):
130 return
131
132 l2 = 2*len(x)
133 x2 = range(0,l2)
134 y2 = range(0,l2)
135
136 for i in range(0,l2):
137 x2[i] = x[i/2]
138
139 y2[0] = 0
140 for i in range(1,l2):
141 y2[i] = y[(i-1)/2]
142
143 self.plot(x2, y2, fmt)
144
145
146 def hold(self, hold=True):
147 """
148 Buffer graphics until subsequently released.
149 """
150 self.buffering = hold
151
152
153 def legend(self, loc=1):
154 """
155 Add a legend to the plot.
156
157 Any other value for loc else disables the legend:
158 1: upper right
159 2: upper left
160 3: lower left
161 4: lower right
162 5: right
163 6: center left
164 7: center right
165 8: lower center
166 9: upper center
167 10: center
168
169 """
170 if 1 > loc > 10: loc = 0
171 self.loc = loc
172 self.show()
173
174
175 def map(self):
176 """
177 Reveal the ASAPlot graphics window and bring it to the top of the
178 window stack.
179 """
180 self.window.wm_deiconify()
181 self.window.lift()
182
183
184 def palette(self, pen=None, colours=None):
185 """
186 Redefine the colour sequence.
187
188 pen is the pen number to use for the next plot; this will be auto-
189 incremented.
190
191 colours is the list of pen colours. Colour may be specified via
192 the single letter values understood by matplotlib:
193
194 b: blue
195 g: green
196 r: red
197 c: cyan
198 m: magenta
199 y: yellow
200 k: black
201 w: white
202
203 or via the full name as listed in the colour dictionary which is
204 loaded by default by load_colours() from rgb.txt and listed by
205 list_colours().
206 """
207
208 if pen is None and colours is None:
209 self.colours = []
210 return
211
212 if pen is None:
213 if not len(self.colours):
214 self.colours = [1]
215 else:
216 self.colours[0] = pen
217
218 if colours is None:
219 return
220
221 cols = []
222 for col in colours:
223 cols.append(get_colour(col))
224
225 self.colours[1:] = cols
226
227 if 0 > self.colours[0] > len(self.colours):
228 self.colours[0] = 1
229
230
231 def plot(self, x=None, y=None, mask=None, fmt=None, add=None):
232 """
233 Plot the next line in the current frame using the current line
234 attributes. The ASAPlot graphics window will be mapped and raised.
235
236 The argument list works a bit like the matlab plot() function.
237 """
238
239 if x is None:
240 if y is None: return
241 x = range(len(y))
242
243 elif y is None:
244 y = x
245 x = range(len(y))
246
247 if mask is None:
248 if fmt is None:
249 line = self.axes.plot(x, y)
250 else:
251 line = self.axes.plot(x, y, fmt)
252 else:
253 segments = []
254
255 mask = list(mask)
256 i = 0
257 while mask[i:].count(1):
258 i += mask[i:].index(1)
259 if mask[i:].count(0):
260 j = i + mask[i:].index(0)
261 else:
262 j = len(mask)
263
264 segments.append(x[i:j])
265 segments.append(y[i:j])
266
267 i = j
268
269 line = self.axes.plot(*segments)
270
271 # Add to an existing line?
272 if add is None or len(self.lines) < add < 0:
273 # Don't add.
274 self.lines.append(line)
275 i = len(self.lines) - 1
276 else:
277 if add == 0: add = len(self.lines)
278 i = add - 1
279 self.lines[i].extend(line)
280
281 # Set/reset attributes for the line.
282 gotcolour = False
283 for k, v in self.attributes.iteritems():
284 if k == 'color': gotcolour = True
285 for segment in self.lines[i]:
286 getattr(segment, "set_%s"%k)(v)
287
288 if not gotcolour and len(self.colours):
289 for segment in self.lines[i]:
290 getattr(segment, "set_color")(self.colours[self.colours[0]])
291
292 self.colours[0] += 1
293 if self.colours[0] >= len(self.colours):
294 self.colours[0] = 1
295
296 self.show()
297
298
299 def quit(self):
300 """
301 Destroy the ASAPlot graphics window.
302 """
303 self.window.destroy()
304
305
306 def register(self, type=None, func=None):
307 """
308 Register, reregister, or deregister events of type 'button_press',
309 'button_release', or 'motion_notify'.
310
311 The specified callback function should have the following signature:
312
313 def func(event)
314
315 where event is an MplEvent instance containing the following data:
316
317 name # Event name.
318 canvas # FigureCanvas instance generating the event.
319 x = None # x position - pixels from left of canvas.
320 y = None # y position - pixels from bottom of canvas.
321 button = None # Button pressed: None, 1, 2, 3.
322 key = None # Key pressed: None, chr(range(255)), shift,
323 win, or control
324 inaxes = None # Axes instance if cursor within axes.
325 xdata = None # x world coordinate.
326 ydata = None # y world coordinate.
327
328 For example:
329
330 def mouse_move(event):
331 print event.xdata, event.ydata
332
333 a = asaplot()
334 a.register('motion_notify', mouse_move)
335
336 If func is None, the event is deregistered.
337
338 Note that in TkAgg keyboard button presses don't generate an event.
339 """
340
341 if not self.events.has_key(type): return
342
343 if func is None:
344 if self.events[type] is not None:
345 # It's not clear that this does anything.
346 self.canvas.mpl_disconnect(self.events[type])
347 self.events[type] = None
348
349 # It seems to be necessary to return events to the toolbar.
350 if type == 'motion_notify':
351 self.canvas.mpl_connect(type + '_event',
352 self.figmgr.toolbar.mouse_move)
353 elif type == 'button_press':
354 self.canvas.mpl_connect(type + '_event',
355 self.figmgr.toolbar.press)
356 elif type == 'button_release':
357 self.canvas.mpl_connect(type + '_event',
358 self.figmgr.toolbar.release)
359
360 else:
361 self.events[type] = self.canvas.mpl_connect(type + '_event', func)
362
363
364 def release(self):
365 """
366 Release buffered graphics.
367 """
368 self.buffering = False
369 self.show()
370
371
372 def set_axes(self, what=None, *args, **kwargs):
373 """
374 Set attributes for the axes by calling the relevant Axes.set_*()
375 method. Colour translation is done as described in the doctext
376 for palette().
377 """
378
379 if what is None: return
380 if what[-6:] == 'colour': what = what[:-6] + 'color'
381
382 newargs = {}
383 for k, v in kwargs.iteritems():
384 k = k.lower()
385 if k == 'colour': k = 'color'
386
387 if k == 'color':
388 v = get_colour(v)
389
390 newargs[k] = v
391
392 getattr(self.axes, "set_%s"%what)(*args, **newargs)
393 self.show()
394
395
396 def set_figure(self, what=None, *args, **kwargs):
397 """
398 Set attributes for the figure by calling the relevant Figure.set_*()
399 method. Colour translation is done as described in the doctext
400 for palette().
401 """
402
403 if what is None: return
404 if what[-6:] == 'colour': what = what[:-6] + 'color'
405 if what[-5:] == 'color' and len(args):
406 args = (get_colour(args[0]),)
407
408 newargs = {}
409 for k, v in kwargs.iteritems():
410 k = k.lower()
411 if k == 'colour': k = 'color'
412
413 if k == 'color':
414 v = get_colour(v)
415
416 newargs[k] = v
417
418 getattr(self.figure, "set_%s"%what)(*args, **newargs)
419 self.show()
420
421
422 def set_line(self, number=None, **kwargs):
423 """
424 Set attributes for the specified line, or else the next line(s)
425 to be plotted.
426
427 number is the 0-relative number of a line that has already been
428 plotted. If no such line exists, attributes are recorded and used
429 for the next line(s) to be plotted.
430
431 Keyword arguments specify Line2D attributes, e.g. color='r'. Do
432
433 import matplotlib
434 help(matplotlib.lines)
435
436 The set_* methods of class Line2D define the attribute names and
437 values. For non-US usage, "colour" is recognized as synonymous with
438 "color".
439
440 Set the value to None to delete an attribute.
441
442 Colour translation is done as described in the doctext for palette().
443 """
444
445 redraw = False
446 for k, v in kwargs.iteritems():
447 k = k.lower()
448 if k == 'colour': k = 'color'
449
450 if k == 'color':
451 v = get_colour(v)
452
453 if 0 <= number < len(self.lines):
454 if self.lines[number] is not None:
455 for line in self.lines[number]:
456 getattr(line, "set_%s"%k)(v)
457 redraw = True
458 else:
459 if v is None:
460 del self.attributes[k]
461 else:
462 self.attributes[k] = v
463
464 if redraw: self.show()
465
466
467 def set_panels(self, rows=1, cols=0, n=-1):
468 """
469 Set the panel layout.
470
471 rows and cols, if cols != 0, specify the number of rows and columns in
472 a regular layout. (Indexing of these panels in matplotlib is row-
473 major, i.e. column varies fastest.)
474
475 cols == 0 is interpreted as a retangular layout that accomodates
476 'rows' panels, e.g. rows == 6, cols == 0 is equivalent to
477 rows == 2, cols == 3.
478
479 0 <= n < rows*cols is interpreted as the 0-relative panel number in
480 the configuration specified by rows and cols to be added to the
481 current figure as its next 0-relative panel number (i). This allows
482 non-regular panel layouts to be constructed via multiple calls. Any
483 other value of n clears the plot and produces a rectangular array of
484 empty panels.
485 """
486 if n < 0 and len(self.subplots):
487 self.figure.clear()
488 self.set_title()
489
490 if rows < 1:
491 rows = 1
492
493 if cols == 0:
494 i = int(sqrt(rows))
495 if i*i < rows: i += 1
496 cols = i
497
498 if i*(i-1) >= rows: i -= 1
499 rows = i
500
501 if 0 <= n < rows*cols:
502 i = len(self.subplots)
503 self.subplots.append({})
504 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
505 cols, n+1)
506 self.subplots[i]['lines'] = []
507
508 if i == 0: self.subplot(0)
509
510 else:
511 self.subplots = []
512 for i in range(0,rows*cols):
513 self.subplots.append({})
514 self.subplots[i]['axes'] = self.figure.add_subplot(rows,
515 cols, i+1)
516 self.subplots[i]['lines'] = []
517
518 self.subplot(0)
519
520
521 def set_title(self, title=None):
522 """
523 Set the title of the plot window. Use the previous title if title is
524 omitted.
525 """
526 if title is not None:
527 self.title = title
528
529 self.figure.text(0.5, 0.95, self.title, horizontalalignment='center')
530
531
532 def show(self):
533 """
534 Show graphics dependent on the current buffering state.
535 """
536 if not self.buffering:
537 if self.loc:
538 lines = []
539 labels = []
540 i = 0
541 for line in self.lines:
542 i += 1
543 if line is not None:
544 lines.append(line[0])
545 lbl = line[0].get_label()
546 if lbl == '':
547 lbl = str(i)
548 labels.append(lbl)
549
550 if len(lines):
551 self.axes.legend(tuple(lines), tuple(labels), self.loc)
552 else:
553 self.axes.legend((' '))
554
555 self.window.wm_deiconify()
556 self.canvas.show()
557
558
559 def subplot(self, i=None, inc=None):
560 """
561 Set the subplot to the 0-relative panel number as defined by one or
562 more invokations of set_panels().
563 """
564 l = len(self.subplots)
565 if l:
566 if i is not None:
567 self.i = i
568
569 if inc is not None:
570 self.i += inc
571
572 self.i %= l
573 self.axes = self.subplots[self.i]['axes']
574 self.lines = self.subplots[self.i]['lines']
575
576
577 def terminate(self):
578 """
579 Clear the figure.
580 """
581 self.window.destroy()
582
583
584 def text(self, *args, **kwargs):
585 """
586 Add text to the figure.
587 """
588 self.figure.text(*args, **kwargs)
589 self.show()
590
591
592 def unmap(self):
593 """
594 Hide the ASAPlot graphics window.
595 """
596 self.window.wm_withdraw()
597
598
599def get_colour(colour='black'):
600 """
601 Look up a colour by name in the colour dictionary. Matches are
602 case-insensitive, insensitive to blanks, and 'gray' matches 'grey'.
603 """
604
605 if colour is None: return None
606
607 if match('[rgbcmykw]$', colour): return colour
608 if match('#[\da-fA-F]{6}$', colour): return colour
609
610 if len(colours) == 0: load_colours()
611
612 # Try a quick match.
613 if colours.has_key(colour): return colours[colour]
614
615 colour = colour.replace(' ','').lower()
616 colour = colour.replace('gray','grey')
617 for name in colours.keys():
618 if name.lower() == colour:
619 return colours[name]
620
621 return '#000000'
622
623
624def list_colours():
625 """
626 List the contents of the colour dictionary sorted by name.
627 """
628
629 if len(colours) == 0: load_colours()
630
631 names = colours.keys()
632 names.sort()
633 for name in names:
634 print colours[name], name
635
636
637def load_colours(file='/usr/local/lib/rgb.txt'):
638 """
639 Load the colour dictionary from the specified file.
640 """
641 print 'Loading colour dictionary from', file
642 rgb = open(file, 'r')
643
644 while True:
645 line = rgb.readline()
646 if line == '': break
647 tmp = line.split()
648
649 if len(tmp) == 4:
650 if tmp[3][:4] == 'gray': continue
651 if tmp[3].lower().find('gray') != -1: continue
652
653 name = tmp[3][0].upper() + tmp[3][1:]
654 r, g, b = int(tmp[0]), int(tmp[1]), int(tmp[2])
655 colours[name] = '#%2.2x%2.2x%2.2x' % (r, g, b)
Note: See TracBrowser for help on using the repository browser.