source: trunk/python/asaplot.py @ 195

Last change on this file since 195 was 187, checked in by mar637, 19 years ago

Bug fix: Handling destruction of window via self._is_dead

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