source: trunk/python/asaplot.py@ 165

Last change on this file since 165 was 120, checked in by cal103, 20 years ago

Added function register() to handle mouse events.

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