source: trunk/python/asaplot.py @ 376

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