source: trunk/python/asaplot.py@ 390

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