Logo Search packages:      
Sourcecode: matplotlib version File versions

legend.py

00001 """
Place a legend on the axes at location loc.  Labels are a
sequence of strings and loc can be a string or an integer
specifying the legend location

The location codes are

  'best'         : 0,
  'upper right'  : 1,  (default)
  'upper left'   : 2,
  'lower left'   : 3,
  'lower right'  : 4,
  'right'        : 5,
  'center left'  : 6,
  'center right' : 7,
  'lower center' : 8,
  'upper center' : 9,
  'center'       : 10,

Return value is a sequence of text, line instances that make
up the legend
"""
from __future__ import division
import sys, warnings
from numerix import array, ones, Float


from matplotlib import verbose, rcParams
from artist import Artist
from cbook import enumerate, is_string_like, iterable, silent_list
from font_manager import FontProperties
from lines import Line2D
from mlab import linspace, segments_intersect
from patches import Patch, Rectangle, RegularPolygon, Shadow, bbox_artist, draw_bbox
from collections import LineCollection, RegularPolyCollection, PatchCollection
from text import Text
from transforms import Bbox, Point, Value, get_bbox_transform, bbox_all,\
     unit_bbox, inverse_transform_bbox, lbwh_to_bbox




00043 def line_cuts_bbox(line, bbox):
    """ Return True if and only if line cuts bbox. """
    minx, miny, width, height = bbox.get_bounds()
    maxx = minx + width
    maxy = miny + height

    n = len(line)
    if n == 0:
        return False

    if n == 1:
        return bbox.contains(line[0][0], line[0][1])
    p1 = line[0]
    for p2 in line[1:]:
        segment = (p1, p2)
        # See if the segment cuts any of the edges of bbox
        for edge in (((minx, miny), (minx, maxy)),
                     ((minx, miny), (maxx, miny)),
                     ((maxx, miny), (maxx, maxy)),
                     ((minx, maxy), (maxx, maxy))):
            if segments_intersect(segment, edge):
                return True
        p1=p2

    return False


00070 class Legend(Artist):
    """
    Place a legend on the axes at location loc.  Labels are a
    sequence of strings and loc can be a string or an integer
    specifying the legend location

    The location codes are

      'best'         : 0,
      'upper right'  : 1,  (default)
      'upper left'   : 2,
      'lower left'   : 3,
      'lower right'  : 4,
      'right'        : 5,
      'center left'  : 6,
      'center right' : 7,
      'lower center' : 8,
      'upper center' : 9,
      'center'       : 10,

    Return value is a sequence of text, line instances that make
    up the legend
    """


    codes = {'best'         : 0,
             'upper right'  : 1,  # default
             'upper left'   : 2,
             'lower left'   : 3,
             'lower right'  : 4,
             'right'        : 5,
             'center left'  : 6,
             'center right' : 7,
             'lower center' : 8,
             'upper center' : 9,
             'center'       : 10,
             }



    zorder = 5
00111     def __init__(self, parent, handles, labels, loc,
                 isaxes= None,
                 numpoints = None,      # the number of points in the legend line
                 prop = None,
                 pad = None,          # the fractional whitespace inside the legend border
                 markerscale = None,    # the relative size of legend markers vs. original
                 # the following dimensions are in axes coords
                 labelsep = None,     # the vertical space between the legend entries
                 handlelen = None,     # the length of the legend lines
                 handletextsep = None, # the space between the legend line and legend text
                 axespad = None,       # the border between the axes and legend edge

                 shadow= None,
                 ):
        """
  parent                # the artist that contains the legend
  handles               # a list of artists (lines, patches) to add to the legend
  labels                # a list of strings to label the legend
  loc                   # a location code
  isaxes=True           # whether this is an axes legend
  numpoints = 4         # the number of points in the legend line
  prop = FontProperties(size='smaller')  # the font property
  pad = 0.2             # the fractional whitespace inside the legend border
  markerscale = 0.6     # the relative size of legend markers vs. original
  shadow                # if True, draw a shadow behind legend

The following dimensions are in axes coords
  labelsep = 0.005     # the vertical space between the legend entries
  handlelen = 0.05     # the length of the legend lines
  handletextsep = 0.02 # the space between the legend line and legend text
  axespad = 0.02       # the border between the axes and legend edge
        """
        Artist.__init__(self)
        if is_string_like(loc) and not self.codes.has_key(loc):
            warnings.warn('Unrecognized location %s. Falling back on upper right; valid locations are\n%s\t' %(loc, '\n\t'.join(self.codes.keys())))
        if is_string_like(loc): loc = self.codes.get(loc, 1)

        proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow, isaxes]
        propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow', 'isaxes']
        for name, value in zip(propnames,proplist):
            if value is None:
                value=rcParams["legend."+name]
            setattr(self,name,value)
        if prop is None:
            self.prop=FontProperties(size=rcParams["legend.fontsize"])
        else:
            self.prop=prop
        self.fontsize = self.prop.get_size_in_points()

        if self.isaxes:  # parent is an Axes
            self.set_figure(parent.figure)
        else:        # parent is a Figure
            self.set_figure(parent)

        self.parent = parent
        self.set_transform( get_bbox_transform( unit_bbox(), parent.bbox) )
        self._loc = loc

        # make a trial box in the middle of the axes.  relocate it
        # based on it's bbox
        left, top = 0.5, 0.5
        if self.numpoints == 1:
            self._xdata = array([left + self.handlelen*0.5])
        else:
            self._xdata = linspace(left, left + self.handlelen, self.numpoints)
        textleft = left+ self.handlelen+self.handletextsep
        self.texts = self._get_texts(labels, textleft, top)
        self.legendHandles = self._get_handles(handles, self.texts)


        if len(self.texts):
            left, top = self.texts[-1].get_position()
            HEIGHT = self._approx_text_height()*len(self.texts)
        else:
            HEIGHT = 0.2

        bottom = top-HEIGHT
        left -= self.handlelen + self.handletextsep + self.pad
        self.legendPatch = Rectangle(
            xy=(left, bottom), width=0.5, height=HEIGHT,
            facecolor='w', edgecolor='k',
            )
        self._set_artist_props(self.legendPatch)
        self._drawFrame = True

    def _set_artist_props(self, a):
        a.set_figure(self.figure)
        a.set_transform(self.get_transform())

    def _approx_text_height(self):
        return self.fontsize/72.0*self.figure.dpi.get()/self.parent.bbox.height()


    def draw(self, renderer):
        if not self.get_visible(): return
        renderer.open_group('legend')
        self._update_positions(renderer)
        if self._drawFrame:
            if self.shadow:
                shadow = Shadow(self.legendPatch, -0.005, -0.005)
                shadow.draw(renderer)
            self.legendPatch.draw(renderer)


        if not len(self.legendHandles) and not len(self.texts): return
        for h in self.legendHandles:
            if h is not None:
                h.draw(renderer)
                if 0: bbox_artist(h, renderer)

        for t in self.texts:
            if 0: bbox_artist(t, renderer)
            t.draw(renderer)
        renderer.close_group('legend')
        #draw_bbox(self.save, renderer, 'g')
        #draw_bbox(self.ibox, renderer, 'r', self.get_transform())

    def _get_handle_text_bbox(self, renderer):
        'Get a bbox for the text and lines in axes coords'

        bboxesText = [t.get_window_extent(renderer) for t in self.texts]
        bboxesHandles = [h.get_window_extent(renderer) for h in self.legendHandles if h is not None]


        bboxesAll = bboxesText
        bboxesAll.extend(bboxesHandles)
        bbox = bbox_all(bboxesAll)
        self.save = bbox

        ibox =  inverse_transform_bbox(self.get_transform(), bbox)
        self.ibox = ibox

        return ibox

    def _get_handles(self, handles, texts):
        HEIGHT = self._approx_text_height()

        ret = []   # the returned legend lines

        for handle, label in zip(handles, texts):
            x, y = label.get_position()
            x -= self.handlelen + self.handletextsep
            if isinstance(handle, Line2D):
                ydata = (y-HEIGHT/2)*ones(self._xdata.shape, Float)
                legline = Line2D(self._xdata, ydata)
                legline.update_from(handle)
                self._set_artist_props(legline) # after update
                legline.set_clip_box(None)
                legline.set_markersize(self.markerscale*legline.get_markersize())

                ret.append(legline)
            elif isinstance(handle, Patch):

                p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT),
                              width = self.handlelen, height=HEIGHT/2,
                              )
                p.update_from(handle)
                self._set_artist_props(p)
                p.set_clip_box(None)
                ret.append(p)
            elif isinstance(handle, LineCollection):
                ydata = (y-HEIGHT/2)*ones(self._xdata.shape, Float)
                legline = Line2D(self._xdata, ydata)
                self._set_artist_props(legline)
                legline.set_clip_box(None)
                lw = handle.get_linewidth()[0]
                dashes = handle.get_dashes()
                color = handle.get_colors()[0]
                legline.set_color(color)
                legline.set_linewidth(lw)
                legline.set_dashes(dashes)
                ret.append(legline)

            elif isinstance(handle, RegularPolyCollection):
                p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT),
                              width = self.handlelen, height=HEIGHT/2,
                              )
                p.set_facecolor(handle._facecolors[0])
                if handle._edgecolors != 'None':
                    p.set_edgecolor(handle._edgecolors[0])
                self._set_artist_props(p)
                p.set_clip_box(None)
                ret.append(p)

            else:
                ret.append(None)

        return ret

00300     def _auto_legend_data(self):
        """ Returns list of vertices and extents covered by the plot.

        Returns a two long list.

        First element is a list of (x, y) vertices (in
        axes-coordinates) covered by all the lines and line
        collections, in the legend's handles.

        Second element is a list of bounding boxes for all the patches in
        the legend's handles.
        """

        if not self.isaxes:
            raise Exception, 'Auto legends not available for figure legends.'

        def get_handles(ax):
            handles = ax.lines[:]
            handles.extend(ax.patches)
            handles.extend([c for c in ax.collections if isinstance(c, LineCollection)])

            return handles

        ax = self.parent
        handles = get_handles(ax)
        vertices = []
        bboxes = []
        lines = []

        inv = ax.transAxes.inverse_xy_tup
        for handle in handles:

            if isinstance(handle, Line2D):

                xdata = handle.get_xdata(orig=False)
                ydata = handle.get_ydata(orig=False)
                trans = handle.get_transform()
                xt, yt = trans.numerix_x_y(xdata, ydata)

                # XXX need a special method in transform to do a list of verts
                averts = [inv(v) for v in zip(xt, yt)]
                lines.append(averts)

            elif isinstance(handle, Patch):

                verts = handle.get_verts()
                trans = handle.get_transform()
                tverts = trans.seq_xy_tups(verts)

                averts = [inv(v) for v in tverts]

                bbox = unit_bbox()
                bbox.update(averts, True)
                bboxes.append(bbox)

            elif isinstance(handle, LineCollection):
                hlines = handle.get_lines()
                trans = handle.get_transform()
                for line in hlines:
                    tline = trans.seq_xy_tups(line)
                    aline = [inv(v) for v in tline]
                    lines.append(aline)

        return [vertices, bboxes, lines]

    def draw_frame(self, b):
        'b is a boolean.  Set draw frame to b'
        self._drawFrame = b

    def get_frame(self):
        'return the Rectangle instance used to frame the legend'
        return self.legendPatch

    def get_lines(self):
        'return a list of lines.Line2D instances in the legend'
        return [h for h in self.legendHandles if isinstance(h, Line2D)]

    def get_patches(self):
        'return a list of patch instances in the legend'
        return silent_list('Patch', [h for h in self.legendHandles if isinstance(h, Patch)])

    def get_texts(self):
        'return a list of text.Text instance in the legend'
        return silent_list('Text', self.texts)

    def _get_texts(self, labels, left, upper):

        # height in axes coords
        HEIGHT = self._approx_text_height()
        pos = upper
        x = left

        ret = []  # the returned list of text instances
        for l in labels:
            text = Text(
                x=x, y=pos,
                text=l,
                fontproperties=self.prop,
                verticalalignment='top',
                horizontalalignment='left',
                )
            self._set_artist_props(text)
            ret.append(text)
            pos -= HEIGHT

        return ret


    def get_window_extent(self):
        return self.legendPatch.get_window_extent()


    def _offset(self, ox, oy):
        'Move all the artists by ox,oy (axes coords)'
        for t in self.texts:
            x,y = t.get_position()
            t.set_position( (x+ox, y+oy) )

        for h in self.legendHandles:
            if isinstance(h, Line2D):
                x,y = h.get_xdata(orig=False), h.get_ydata(orig=False)
                h.set_data( x+ox, y+oy)
            elif isinstance(h, Rectangle):
                h.xy[0] = h.xy[0] + ox
                h.xy[1] = h.xy[1] + oy
            elif isinstance(h, RegularPolygon):
                h.verts = [(x + ox, y + oy) for x, y in h.verts]

        x, y = self.legendPatch.get_x(), self.legendPatch.get_y()
        self.legendPatch.set_x(x+ox)
        self.legendPatch.set_y(y+oy)

00432     def _find_best_position(self, width, height, consider=None):
        """Determine the best location to place the legend.

        `consider` is a list of (x, y) pairs to consider as a potential
        lower-left corner of the legend. All are axes coords.
        """

        verts, bboxes, lines = self._auto_legend_data()

        consider = [self._loc_to_axes_coords(x, width, height) for x in range(1, len(self.codes))]

        tx, ty = self.legendPatch.xy

        candidates = []
        for l, b in consider:
            legendBox = lbwh_to_bbox(l, b, width, height)
            badness = 0
            badness = legendBox.count_contains(verts)
            ox, oy = l-tx, b-ty
            for bbox in bboxes:
                if legendBox.overlaps(bbox):
                    badness += 1

            for line in lines:
                if line_cuts_bbox(line, legendBox):
                    badness += 1

            if badness == 0:
                return ox, oy

            candidates.append((badness, (ox, oy)))

        # rather than use min() or list.sort(), do this so that we are assured
        # that in the case of two equal badnesses, the one first considered is
        # returned.
        minCandidate = candidates[0]
        for candidate in candidates:
            if candidate[0] < minCandidate[0]:
                minCandidate = candidate

        ox, oy = minCandidate[1]

        return ox, oy


00477     def _loc_to_axes_coords(self, loc, width, height):
        """Convert a location code to axes coordinates.

        - loc: a location code, which may be a pair of literal axes coords, or
          in range(1, 11). This coresponds to the possible values for
          self._loc, excluding "best".

        - width, height: the final size of the legend, axes units.
        """
        BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)

        left = self.axespad
        right = 1.0 - (self.axespad + width)
        upper = 1.0 - (self.axespad + height)
        lower = self.axespad
        centerx = 0.5 - (width/2.0)
        centery = 0.5 - (height/2.0)

        if loc == UR:
            return right, upper
        if loc == UL:
            return left, upper
        if loc == LL:
            return left, lower
        if loc == LR:
            return right, lower
        if loc == CL:
            return left, centery
        if loc in (CR, R):
            return right, centery
        if loc == LC:
            return centerx, lower
        if loc == UC:
            return centerx, upper
        if loc == C:
            return centerx, centery
        raise TypeError, "%r isn't an understood type code." % (loc,)

    def _update_positions(self, renderer):
        # called from renderer to allow more precise estimates of
        # widths and heights with get_window_extent

        if not len(self.legendHandles) and not len(self.texts): return
        def get_tbounds(text):  #get text bounds in axes coords
            bbox = text.get_window_extent(renderer)
            bboxa = inverse_transform_bbox(self.get_transform(), bbox)
            return bboxa.get_bounds()

        hpos = []
        for t, tabove in zip(self.texts[1:], self.texts[:-1]):
            x,y = t.get_position()
            l,b,w,h = get_tbounds(tabove)
            b -= self.labelsep
            h += 2*self.labelsep
            hpos.append( (b,h) )
            t.set_position( (x, b-0.1*h) )

        # now do the same for last line
        l,b,w,h = get_tbounds(self.texts[-1])
        b -= self.labelsep
        h += 2*self.labelsep
        hpos.append( (b,h) )

        for handle, tup in zip(self.legendHandles, hpos):
            y,h = tup
            if isinstance(handle, Line2D):
                ydata = y*ones(self._xdata.shape, Float)
                handle.set_ydata(ydata+h/2)
            elif isinstance(handle, Rectangle):
                handle.set_y(y+1/4*h)
                handle.set_height(h/2)

        # Set the data for the legend patch
        bbox = self._get_handle_text_bbox(renderer).deepcopy()

        bbox.scale(1 + self.pad, 1 + self.pad)
        l,b,w,h = bbox.get_bounds()
        self.legendPatch.set_bounds(l,b,w,h)

        BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
        ox, oy = 0, 0                           # center


        if iterable(self._loc) and len(self._loc)==2:
            xo = self.legendPatch.get_x()
            yo = self.legendPatch.get_y()
            x, y = self._loc
            ox = x-xo
            oy = y-yo
            self._offset(ox, oy)
        else:
            if self._loc in (BEST,):
                ox, oy = self._find_best_position(w, h)
            if self._loc in (UL, LL, CL):           # left
                ox = self.axespad - l
            if self._loc in (UR, LR, R, CR):  # right
                ox = 1 - (l + w + self.axespad)
            if self._loc in (UR, UL, UC):     # upper
                oy = 1 - (b + h + self.axespad)
            if self._loc in (LL, LR, LC):           # lower
                oy = self.axespad - b
            if self._loc in (LC, UC, C):            # center x
                ox = (0.5-w/2)-l
            if self._loc in (CL, CR, C):            # center y
                oy = (0.5-h/2)-b
            self._offset(ox, oy)


#artist.kwdocd['Legend'] = kwdoc(Legend)

Generated by  Doxygen 1.6.0   Back to index