source: trunk/python/asaplot.py@ 198

Last change on this file since 198 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
RevLine 
[111]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, \
[117]15 FigureManagerTkAgg
[111]16from matplotlib.figure import Figure, Text
[118]17from matplotlib.numerix import sqrt
[111]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
[118]30 def __init__(self, rows=1, cols=0, title='', size=(8,6), buffering=False):
[117]31 """
32 Create a new instance of the ASAPlot plotting class.
[119]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().
[117]36 """
37 self.window = Tk.Tk()
[187]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)
[117]44 self.frame1 = Tk.Frame(self.window, relief=Tk.RIDGE, borderwidth=4)
45 self.frame1.pack(fill=Tk.BOTH)
[111]46
[117]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)
[111]50
[117]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')
[111]54
[120]55 self.events = {'button_press':None,
56 'button_release':None,
57 'motion_notify':None}
58
[118]59 self.set_title(title)
[117]60 self.subplots = []
[118]61 if rows > 0:
62 self.set_panels(rows, cols)
[111]63
64
[117]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
[111]69
[117]70 matplotlib.interactive = True
71 self.buffering = buffering
[111]72
[117]73 self.canvas.show()
[111]74
75
[117]76 def clear(self):
77 """
78 Delete all lines from the plot. Line numbering will restart from 1.
79 """
[111]80
[117]81 for i in range(1,len(self.lines)+1):
82 self.delete(i)
[111]83
[117]84 self.axes.clear()
85 self.colours[0] = 1
86 self.lines = []
87
[111]88
89 def delete(self, numbers=None):
[117]90 """
91 Delete the 0-relative line number, default is to delete the last.
92 The remaining lines are NOT renumbered.
93 """
[111]94
[117]95 if numbers is None: numbers = [len(self.lines)-1]
[111]96
[117]97 if not hasattr(numbers, '__iter__'):
98 numbers = [numbers]
[111]99
[117]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
[111]106
[117]107 self.show()
[111]108
109
110 def get_line(self):
[117]111 """
112 Get the current default line attributes.
113 """
114 return self.attributes
[111]115
116
[119]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
[111]146 def hold(self, hold=True):
[117]147 """
148 Buffer graphics until subsequently released.
149 """
150 self.buffering = hold
[111]151
152
153 def legend(self, loc=1):
[117]154 """
155 Add a legend to the plot.
[111]156
[117]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
[111]168
[117]169 """
170 if 1 > loc > 10: loc = 0
171 self.loc = loc
172 self.show()
[111]173
174
175 def map(self):
[117]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()
[111]182
[117]183
[111]184 def palette(self, pen=None, colours=None):
[117]185 """
186 Redefine the colour sequence.
[111]187
[117]188 pen is the pen number to use for the next plot; this will be auto-
189 incremented.
[111]190
[117]191 colours is the list of pen colours. Colour may be specified via
192 the single letter values understood by matplotlib:
[111]193
[117]194 b: blue
195 g: green
196 r: red
197 c: cyan
198 m: magenta
199 y: yellow
200 k: black
201 w: white
[111]202
[117]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 """
[111]207
[117]208 if pen is None and colours is None:
209 self.colours = []
210 return
[111]211
[117]212 if pen is None:
213 if not len(self.colours):
214 self.colours = [1]
215 else:
216 self.colours[0] = pen
[111]217
[117]218 if colours is None:
219 return
[111]220
[117]221 cols = []
222 for col in colours:
223 cols.append(get_colour(col))
[111]224
[117]225 self.colours[1:] = cols
[111]226
[117]227 if 0 > self.colours[0] > len(self.colours):
228 self.colours[0] = 1
[111]229
230
231 def plot(self, x=None, y=None, mask=None, fmt=None, add=None):
[117]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.
[111]235
[117]236 The argument list works a bit like the matlab plot() function.
237 """
[111]238
[117]239 if x is None:
240 if y is None: return
241 x = range(len(y))
[111]242
[117]243 elif y is None:
244 y = x
245 x = range(len(y))
[111]246
[117]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 = []
[111]254
[117]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)
[111]263
[117]264 segments.append(x[i:j])
265 segments.append(y[i:j])
[111]266
[117]267 i = j
[111]268
[117]269 line = self.axes.plot(*segments)
[111]270
[117]271 # Add to an existing line?
272 if add is None or len(self.lines) < add < 0:
[119]273 # Don't add.
[117]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)
[111]280
[117]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)
[111]287
[117]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]])
[111]291
[117]292 self.colours[0] += 1
293 if self.colours[0] >= len(self.colours):
294 self.colours[0] = 1
[111]295
[117]296 self.show()
[111]297
298
299 def quit(self):
[117]300 """
301 Destroy the ASAPlot graphics window.
302 """
303 self.window.destroy()
[111]304
305
[120]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
[111]364 def release(self):
[117]365 """
366 Release buffered graphics.
367 """
368 self.buffering = False
369 self.show()
[111]370
371
372 def set_axes(self, what=None, *args, **kwargs):
[117]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 """
[111]378
[117]379 if what is None: return
380 if what[-6:] == 'colour': what = what[:-6] + 'color'
[111]381
[117]382 newargs = {}
383 for k, v in kwargs.iteritems():
384 k = k.lower()
385 if k == 'colour': k = 'color'
[111]386
[117]387 if k == 'color':
388 v = get_colour(v)
[111]389
[117]390 newargs[k] = v
[111]391
[117]392 getattr(self.axes, "set_%s"%what)(*args, **newargs)
393 self.show()
[111]394
395
396 def set_figure(self, what=None, *args, **kwargs):
[117]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 """
[111]402
[117]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]),)
[111]407
[117]408 newargs = {}
409 for k, v in kwargs.iteritems():
410 k = k.lower()
411 if k == 'colour': k = 'color'
[111]412
[117]413 if k == 'color':
414 v = get_colour(v)
[111]415
[117]416 newargs[k] = v
[111]417
[117]418 getattr(self.figure, "set_%s"%what)(*args, **newargs)
419 self.show()
[111]420
421
422 def set_line(self, number=None, **kwargs):
[117]423 """
424 Set attributes for the specified line, or else the next line(s)
425 to be plotted.
[111]426
[117]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.
[111]430
[117]431 Keyword arguments specify Line2D attributes, e.g. color='r'. Do
[111]432
[117]433 import matplotlib
434 help(matplotlib.lines)
[111]435
[117]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".
[111]439
[117]440 Set the value to None to delete an attribute.
[111]441
[117]442 Colour translation is done as described in the doctext for palette().
443 """
[111]444
[117]445 redraw = False
446 for k, v in kwargs.iteritems():
447 k = k.lower()
448 if k == 'colour': k = 'color'
[111]449
[117]450 if k == 'color':
451 v = get_colour(v)
[111]452
[117]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
[111]463
[117]464 if redraw: self.show()
[111]465
466
[118]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
[119]508 if i == 0: self.subplot(0)
[118]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
[119]518 self.subplot(0)
[118]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
[111]532 def show(self):
[117]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)
[111]549
[117]550 if len(lines):
551 self.axes.legend(tuple(lines), tuple(labels), self.loc)
552 else:
553 self.axes.legend((' '))
[111]554
[117]555 self.window.wm_deiconify()
556 self.canvas.show()
[111]557
558
[119]559 def subplot(self, i=None, inc=None):
[117]560 """
[118]561 Set the subplot to the 0-relative panel number as defined by one or
562 more invokations of set_panels().
[117]563 """
[118]564 l = len(self.subplots)
565 if l:
[119]566 if i is not None:
[120]567 self.i = i
[111]568
[119]569 if inc is not None:
[120]570 self.i += inc
[117]571
[119]572 self.i %= l
573 self.axes = self.subplots[self.i]['axes']
574 self.lines = self.subplots[self.i]['lines']
575
576
[111]577 def terminate(self):
[117]578 """
579 Clear the figure.
580 """
581 self.window.destroy()
[111]582
583
584 def text(self, *args, **kwargs):
[117]585 """
586 Add text to the figure.
587 """
588 self.figure.text(*args, **kwargs)
589 self.show()
[111]590
591
592 def unmap(self):
[117]593 """
594 Hide the ASAPlot graphics window.
595 """
596 self.window.wm_withdraw()
[111]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():
[117]618 if name.lower() == colour:
619 return colours[name]
[111]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:
[117]634 print colours[name], name
[111]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:
[117]645 line = rgb.readline()
646 if line == '': break
647 tmp = line.split()
[111]648
[117]649 if len(tmp) == 4:
650 if tmp[3][:4] == 'gray': continue
651 if tmp[3].lower().find('gray') != -1: continue
[111]652
[117]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.