source: trunk/python/asaplot.py @ 120

Last change on this file since 120 was 120, checked in by cal103, 19 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.