source: trunk/python/asaplot.py @ 174

Last change on this file since 174 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
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()
38       
39        self.frame1 = Tk.Frame(self.window, relief=Tk.RIDGE, borderwidth=4)
40        self.frame1.pack(fill=Tk.BOTH)
[111]41
[117]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)
[111]45
[117]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')
[111]49
[120]50        self.events = {'button_press':None,
51                       'button_release':None,
52                       'motion_notify':None}
53
[118]54        self.set_title(title)
[117]55        self.subplots = []
[118]56        if rows > 0:
57            self.set_panels(rows, cols)
[111]58
59
[117]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
[111]64
[117]65        matplotlib.interactive = True
66        self.buffering = buffering
[111]67
[117]68        self.canvas.show()
[111]69
70
[117]71    def clear(self):
72        """
73        Delete all lines from the plot.  Line numbering will restart from 1.
74        """
[111]75
[117]76        for i in range(1,len(self.lines)+1):
77           self.delete(i)
[111]78
[117]79        self.axes.clear()
80        self.colours[0] = 1
81        self.lines = []
82       
[111]83
84    def delete(self, numbers=None):
[117]85        """
86        Delete the 0-relative line number, default is to delete the last.
87        The remaining lines are NOT renumbered.
88        """
[111]89
[117]90        if numbers is None: numbers = [len(self.lines)-1]
[111]91
[117]92        if not hasattr(numbers, '__iter__'):
93            numbers = [numbers]
[111]94
[117]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
[111]101
[117]102        self.show()
[111]103
104
105    def get_line(self):
[117]106        """
107        Get the current default line attributes.
108        """
109        return self.attributes
[111]110
111
[119]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
[111]141    def hold(self, hold=True):
[117]142        """
143        Buffer graphics until subsequently released.
144        """
145        self.buffering = hold
[111]146
147
148    def legend(self, loc=1):
[117]149        """
150        Add a legend to the plot.
[111]151
[117]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
[111]163
[117]164        """
165        if 1 > loc > 10: loc = 0
166        self.loc = loc
167        self.show()
[111]168
169
170    def map(self):
[117]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()
[111]177
[117]178
[111]179    def palette(self, pen=None, colours=None):
[117]180        """
181        Redefine the colour sequence.
[111]182
[117]183        pen is the pen number to use for the next plot; this will be auto-
184        incremented.
[111]185
[117]186        colours is the list of pen colours.  Colour may be specified via
187        the single letter values understood by matplotlib:
[111]188
[117]189            b: blue
190            g: green
191            r: red
192            c: cyan
193            m: magenta
194            y: yellow
195            k: black
196            w: white
[111]197
[117]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        """
[111]202
[117]203        if pen is None and colours is None:
204            self.colours = []
205            return
[111]206
[117]207        if pen is None:
208            if not len(self.colours):
209                self.colours = [1]
210        else:
211            self.colours[0] = pen
[111]212
[117]213        if colours is None:
214            return
[111]215
[117]216        cols = []
217        for col in colours:
218            cols.append(get_colour(col))
[111]219
[117]220        self.colours[1:] = cols
[111]221
[117]222        if 0 > self.colours[0] > len(self.colours):
223            self.colours[0] = 1
[111]224
225
226    def plot(self, x=None, y=None, mask=None, fmt=None, add=None):
[117]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.
[111]230
[117]231        The argument list works a bit like the matlab plot() function.
232        """
[111]233
[117]234        if x is None:
235            if y is None: return
236            x = range(len(y))
[111]237
[117]238        elif y is None:
239            y = x
240            x = range(len(y))
[111]241
[117]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 = []
[111]249
[117]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)
[111]258
[117]259                segments.append(x[i:j])
260                segments.append(y[i:j])
[111]261
[117]262                i = j
[111]263
[117]264            line = self.axes.plot(*segments)
[111]265
[117]266        # Add to an existing line?
267        if add is None or len(self.lines) < add < 0:
[119]268            # Don't add.
[117]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)
[111]275
[117]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)
[111]282
[117]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]])
[111]286
[117]287            self.colours[0] += 1
288            if self.colours[0] >= len(self.colours):
289                self.colours[0] = 1
[111]290
[117]291        self.show()
[111]292
293
294    def quit(self):
[117]295        """
296        Destroy the ASAPlot graphics window.
297        """
298        self.window.destroy()
[111]299
300
[120]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
[111]359    def release(self):
[117]360        """
361        Release buffered graphics.
362        """
363        self.buffering = False
364        self.show()
[111]365
366
367    def set_axes(self, what=None, *args, **kwargs):
[117]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        """
[111]373
[117]374        if what is None: return
375        if what[-6:] == 'colour': what = what[:-6] + 'color'
[111]376
[117]377        newargs = {}
378        for k, v in kwargs.iteritems():
379            k = k.lower()
380            if k == 'colour': k = 'color'
[111]381
[117]382            if k == 'color':
383                v = get_colour(v)
[111]384
[117]385            newargs[k] = v
[111]386
[117]387        getattr(self.axes, "set_%s"%what)(*args, **newargs)
388        self.show()
[111]389
390
391    def set_figure(self, what=None, *args, **kwargs):
[117]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        """
[111]397
[117]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]),)
[111]402
[117]403        newargs = {}
404        for k, v in kwargs.iteritems():
405            k = k.lower()
406            if k == 'colour': k = 'color'
[111]407
[117]408            if k == 'color':
409                v = get_colour(v)
[111]410
[117]411            newargs[k] = v
[111]412
[117]413        getattr(self.figure, "set_%s"%what)(*args, **newargs)
414        self.show()
[111]415
416
417    def set_line(self, number=None, **kwargs):
[117]418        """
419        Set attributes for the specified line, or else the next line(s)
420        to be plotted.
[111]421
[117]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.
[111]425
[117]426        Keyword arguments specify Line2D attributes, e.g. color='r'.  Do
[111]427
[117]428            import matplotlib
429            help(matplotlib.lines)
[111]430
[117]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".
[111]434
[117]435        Set the value to None to delete an attribute.
[111]436
[117]437        Colour translation is done as described in the doctext for palette().
438        """
[111]439
[117]440        redraw = False
441        for k, v in kwargs.iteritems():
442            k = k.lower()
443            if k == 'colour': k = 'color'
[111]444
[117]445            if k == 'color':
446                v = get_colour(v)
[111]447
[117]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
[111]458
[117]459        if redraw: self.show()
[111]460
461
[118]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
[119]503            if i == 0: self.subplot(0)
[118]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
[119]513            self.subplot(0)
[118]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
[111]527    def show(self):
[117]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)
[111]544
[117]545                if len(lines):
546                    self.axes.legend(tuple(lines), tuple(labels), self.loc)
547                else:
548                    self.axes.legend((' '))
[111]549
[117]550            self.window.wm_deiconify()
551            self.canvas.show()
[111]552
553
[119]554    def subplot(self, i=None, inc=None):
[117]555        """
[118]556        Set the subplot to the 0-relative panel number as defined by one or
557        more invokations of set_panels().
[117]558        """
[118]559        l = len(self.subplots)
560        if l:
[119]561            if i is not None:
[120]562                self.i = i
[111]563
[119]564            if inc is not None:
[120]565                self.i += inc
[117]566
[119]567            self.i %= l
568            self.axes  = self.subplots[self.i]['axes']
569            self.lines = self.subplots[self.i]['lines']
570
571
[111]572    def terminate(self):
[117]573        """
574        Clear the figure.
575        """
576        self.window.destroy()
[111]577
578
579    def text(self, *args, **kwargs):
[117]580        """
581        Add text to the figure.
582        """
583        self.figure.text(*args, **kwargs)
584        self.show()
[111]585
586
587    def unmap(self):
[117]588        """
589        Hide the ASAPlot graphics window.
590        """
591        self.window.wm_withdraw()
[111]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():
[117]613        if name.lower() == colour:
614            return colours[name]
[111]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:
[117]629        print colours[name], name
[111]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:
[117]640        line = rgb.readline()
641        if line == '': break
642        tmp = line.split()
[111]643
[117]644        if len(tmp) == 4:
645            if tmp[3][:4] == 'gray': continue
646            if tmp[3].lower().find('gray') != -1: continue
[111]647
[117]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.