From: <jd...@us...> - 2008-12-01 14:51:40
|
Revision: 6461 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=6461&view=rev Author: jdh2358 Date: 2008-12-01 14:51:35 +0000 (Mon, 01 Dec 2008) Log Message: ----------- added Jae Joon's legend and offsetbox implementation Modified Paths: -------------- trunk/matplotlib/CHANGELOG trunk/matplotlib/lib/matplotlib/legend.py trunk/matplotlib/lib/matplotlib/rcsetup.py Added Paths: ----------- trunk/matplotlib/examples/pylab_examples/legend_demo3.py trunk/matplotlib/lib/matplotlib/offsetbox.py Modified: trunk/matplotlib/CHANGELOG =================================================================== --- trunk/matplotlib/CHANGELOG 2008-12-01 14:06:49 UTC (rev 6460) +++ trunk/matplotlib/CHANGELOG 2008-12-01 14:51:35 UTC (rev 6461) @@ -1,3 +1,6 @@ +2008-11-30 Reimplementaion of the legend which supports baseline alignement, + multi-column, and expand mode. - JJL + 2008-12-01 Fixed histogram autoscaling bug when bins or range are given explicitly (fixes Debian bug 503148) - MM Added: trunk/matplotlib/examples/pylab_examples/legend_demo3.py =================================================================== --- trunk/matplotlib/examples/pylab_examples/legend_demo3.py (rev 0) +++ trunk/matplotlib/examples/pylab_examples/legend_demo3.py 2008-12-01 14:51:35 UTC (rev 6461) @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import matplotlib.pyplot as plt +import numpy as np + +def myplot(ax): + t1 = np.arange(0.0, 1.0, 0.1) + for n in [1, 2, 3, 4]: + ax.plot(t1, t1**n, label="n=%d"%(n,)) + +ax1 = plt.subplot(3,1,1) +ax1.plot([1], label="multi\nline") +ax1.plot([1], label="$2^{2^2}$") +ax1.plot([1], label=r"$\frac{1}{2}\pi$") +ax1.legend(loc=1, ncol=3, shadow=True) + +ax2 = plt.subplot(3,1,2) +myplot(ax2) +ax2.legend(loc=1, ncol=2, shadow=True) + + +ax3 = plt.subplot(3,1,3) +myplot(ax3) +ax3.legend(loc=1, ncol=4, mode="expand", shadow=True) + + +#title('Damped oscillation') + +plt.draw() +plt.show() + +#plt.savefig("legend_demo3") + + Modified: trunk/matplotlib/lib/matplotlib/legend.py =================================================================== --- trunk/matplotlib/lib/matplotlib/legend.py 2008-12-01 14:06:49 UTC (rev 6460) +++ trunk/matplotlib/lib/matplotlib/legend.py 2008-12-01 14:51:35 UTC (rev 6461) @@ -26,16 +26,21 @@ import numpy as np from matplotlib import rcParams -from artist import Artist -from cbook import is_string_like, iterable, silent_list, safezip -from font_manager import FontProperties -from lines import Line2D -from mlab import segments_intersect -from patches import Patch, Rectangle, Shadow, bbox_artist -from collections import LineCollection, RegularPolyCollection -from text import Text -from transforms import Affine2D, Bbox, BboxTransformTo +from matplotlib.artist import Artist +from matplotlib.cbook import is_string_like, iterable, silent_list, safezip +from matplotlib.font_manager import FontProperties +from matplotlib.lines import Line2D +from matplotlib.mlab import segments_intersect +from matplotlib.patches import Patch, Rectangle, Shadow, bbox_artist, FancyBboxPatch +from matplotlib.collections import LineCollection, RegularPolyCollection +from matplotlib.text import Text +from matplotlib.transforms import Affine2D, Bbox, BboxTransformTo +from itertools import cycle, izip + +from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea + + class Legend(Artist): """ Place a legend on the axes at location loc. Labels are a @@ -75,7 +80,6 @@ } - zorder = 5 def __str__(self): return "Legend" @@ -83,72 +87,132 @@ def __init__(self, parent, handles, labels, loc = None, numpoints = None, # the number of points in the legend line + markerscale = None, # the relative size of legend markers vs. original scatterpoints = 3, # TODO: may be an rcParam - prop = None, - pad = None, # the fractional whitespace inside the legend border - borderpad = None, - markerscale = None, # the relative size of legend markers vs. original + scatteryoffsets=None, + prop = None, # properties for the legend texts + # 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 + pad = None, # deprecated; use borderpad + labelsep = None, # deprecated; use labelspacing + handlelen = None, # deprecated; use handlelength + handletextsep = None, # deprecated; use handletextpad + axespad = None, # deprecated; use borderaxespad + + # spacing & pad defined as a fractionof the font-size + borderpad = None, # the fractional whitespace inside the legend border + labelspacing=None, #the vertical space between the legend entries + handlelength=None, # the length of the legend handles + handletextpad=None, # the pad between the legend handle and text + borderaxespad=None, # the pad between the axes and legend border + columnspacing=None, # spacing between columns + + ncol=1, # number of columns + mode=None, # mode for horizontal distribution of columns. None, "expand" + shadow = None, - scatteryoffsets=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 - numpoints = 4 # the number of points in the legend line - scatterpoints = 3 # the number of points for the scatterplot legend - 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 - scatteryoffsets # a list of yoffsets for scatter symbols in legend + - *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 -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 + Optional keyword arguments: + + ================ ========================================= + Keyword Description + ================ ========================================= + + loc a location code + numpoints the number of points in the legend line + prop the font property + markerscale the relative size of legend markers vs. original + shadow if True, draw a shadow behind legend + scatteryoffsets a list of yoffsets for scatter symbols in legend + + borderpad the fractional whitespace inside the legend border + labelspacing the vertical space between the legend entries + handlelength the length of the legend handles + handletextpad the pad between the legend handle and text + borderaxespad the pad between the axes and legend border + columnspacing the spacing between columns + +The dimensions of pad and spacing are given as a fraction of the +fontsize. Values from rcParams will be used if None. + """ - from axes import Axes # local import only to avoid circularity - from figure import Figure # local import only to avoid circularity + from matplotlib.axes import Axes # local import only to avoid circularity + from matplotlib.figure import Figure # local import only to avoid circularity Artist.__init__(self) - proplist=[numpoints, scatterpoints, pad, borderpad, markerscale, labelsep, - handlelen, handletextsep, axespad, shadow] - propnames=['numpoints','scatterpoints', 'pad', 'borderpad', 'markerscale', - 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow'] - for name, value in safezip(propnames,proplist): - if value is None: - value=rcParams["legend."+name] - setattr(self,name,value) - if pad: - warnings.warn("Use 'borderpad' instead of 'pad'.", DeprecationWarning) - # 2008/10/04 - if self.numpoints <= 0: - raise ValueError("numpoints must be > 0; it was %d"% numpoints) - if self.scatterpoints <= 0: - raise ValueError("scatterpoints must be > 0; it was %d"% numpoints) if prop is None: self.prop=FontProperties(size=rcParams["legend.fontsize"]) else: self.prop=prop self.fontsize = self.prop.get_size_in_points() + propnames=['numpoints', 'markerscale', 'shadow', "columnspacing", + "scatterpoints"] + + localdict = locals() + + for name in propnames: + if localdict[name] is None: + value = rcParams["legend."+name] + else: + value = localdict[name] + setattr(self, name, value) + + # Take care the deprecated keywords + deprecated_kwds = {"pad":"borderpad", + "labelsep":"labelspacing", + "handlelen":"handlelength", + "handletextsep":"handletextpad", + "axespad":"borderaxespad"} + + # convert values of deprecated keywords (ginve in axes coords) + # to new vaules in a fraction of the font size + + # conversion factor + bbox = parent.bbox + axessize_fontsize = min(bbox.width, bbox.height)/self.fontsize + + for k, v in deprecated_kwds.items(): + # use deprecated value if not None and if their newer + # counter part is None. + if localdict[k] is not None and localdict[v] is None: + warnings.warn("Use '%s' instead of '%s'." % (v, k), + DeprecationWarning) + setattr(self, v, localdict[k]*axessize_fontsize) + continue + + # Otherwise, use new keywords + if localdict[v] is None: + setattr(self, v, rcParams["legend."+v]) + else: + setattr(self, v, localdict[v]) + + del localdict + + self._ncol = ncol + + if self.numpoints <= 0: + raise ValueError("numpoints must be >= 0; it was %d"% numpoints) + # introduce y-offset for handles of the scatter plot if scatteryoffsets is None: - self._scatteryoffsets = np.array([4./8., 5./8., 3./8.]) + self._scatteryoffsets = np.array([3./8., 4./8., 2.5/8.]) else: self._scatteryoffsets = np.asarray(scatteryoffsets) - reps = int(self.scatterpoints / len(self._scatteryoffsets)) + 1 + reps = int(self.numpoints / len(self._scatteryoffsets)) + 1 self._scatteryoffsets = np.tile(self._scatteryoffsets, reps)[:self.scatterpoints] + # _legend_box is an OffsetBox instance that contains all + # legend items and will be initialized from _init_legend_box() + # method. + self._legend_box = None + if isinstance(parent,Axes): self.isaxes = True self.set_figure(parent.figure) @@ -158,9 +222,6 @@ else: raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent - self._offsetTransform = Affine2D() - self._parentTransform = BboxTransformTo(parent.bbox) - Artist.set_transform(self, self._offsetTransform + self._parentTransform) if loc is None: loc = rcParams["legend.loc"] @@ -186,100 +247,165 @@ loc = 1 self._loc = loc + self._mode = mode - self.legendPatch = Rectangle( - xy=(0.0, 0.0), width=0.5, height=0.5, + # We use FancyBboxPatch to draw a legend frame. The location + # and size of the box will be updated during the drawing time. + self.legendPatch = FancyBboxPatch( + xy=(0.0, 0.0), width=1., height=1., facecolor='w', edgecolor='k', + mutation_scale=self.fontsize, ) + + # The width and height of the legendPatch will be set (in the + # draw()) to the length that includes the padding. Thus we set + # pad=0 here. + self.legendPatch.set_boxstyle("round",pad=0, #self.borderpad, + rounding_size=0.2) + self._set_artist_props(self.legendPatch) - # make a trial box in the middle of the axes. relocate it - # based on it's bbox - left, top = 0.5, 0.5 - textleft = left+ self.handlelen+self.handletextsep - self.texts = self._get_texts(labels, textleft, top) - self.legendHandles = self._get_handles(handles, self.texts) - self._drawFrame = True + # populate the legend_box with legend items. + self._init_legend_box(handles, labels) + self._legend_box.set_figure(self.figure) + + def _set_artist_props(self, a): + """ + set the boilerplate props for artists added to axes + """ a.set_figure(self.figure) + + for c in self.get_children(): + c.set_figure(self.figure) + a.set_transform(self.get_transform()) - def _approx_text_height(self): - return self.fontsize/72.0*self.figure.dpi/self.parent.bbox.height + def _findoffset_best(self, width, height, xdescent, ydescent): + "Heper function to locate the legend" + ox, oy = self._find_best_position(width, height) + return ox+xdescent, oy+ydescent + def _findoffset_loc(self, width, height, xdescent, ydescent): + "Heper function to locate the legend" + bbox = Bbox.from_bounds(0, 0, width, height) + x, y = self._get_anchored_bbox(self._loc, bbox, self.parent.bbox) + return x+xdescent, y+ydescent def draw(self, renderer): + "Draw everything that belongs to the legend" if not self.get_visible(): return + renderer.open_group('legend') - self._update_positions(renderer) + + # find_offset function will be provided to _legend_box and + # _legend_box will draw itself at the location of the return + # value of the find_offset. + if self._loc == 0: + self._legend_box.set_offset(self._findoffset_best) + else: + self._legend_box.set_offset(self._findoffset_loc) + + # if mode == fill, set the width of the legend_box to the + # width of the paret (minus pads) + if self._mode in ["expand"]: + pad = 2*(self.borderaxespad+self.borderpad)*self.fontsize + self._legend_box.set_width(self.parent.bbox.width-pad) + if self._drawFrame: + # update the location and size of the legend + bbox = self._legend_box.get_window_extent(renderer) + self.legendPatch.set_bounds(bbox.x0, bbox.y0, + bbox.width, bbox.height) + if self.shadow: - shadow = Shadow(self.legendPatch, -0.005, -0.005) + shadow = Shadow(self.legendPatch, 2, -2) shadow.draw(renderer) + self.legendPatch.draw(renderer) + self._legend_box.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 hasattr(h, '_legmarker'): - h._legmarker.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] + def _approx_text_height(self): + """ + Return the approximate height of the text. This is used to place + the legend handle. + """ + return self.fontsize/72.0*self.figure.dpi - bboxesAll = bboxesText - bboxesAll.extend(bboxesHandles) - bbox = Bbox.union(bboxesAll) - self.save = bbox + def _init_legend_box(self, handles, labels): + """ + Initiallize the legend_box. The legend_box is an instance of + the OffsetBox, which is packed with legend handles and + texts. Once packed, their location is calculated during the + drawing time. + """ - ibox = bbox.inverse_transformed(self.get_transform()) - self.ibox = ibox + # legend_box is a HPacker, horizontally packed with + # columns. Each column is a VPacker, vertically packed with + # legend items. Each legend item is HPacker packed with + # legend handleBox and labelBox. handleBox is an instance of + # offsetbox.DrawingArea which contains legend handle. labelBox + # is an instance of offsetbox.TextArea which contains legend + # text. - return ibox + + text_list = [] # the list of text instances + handle_list = [] # the list of text instances - def _get_handles(self, handles, texts): - handles = list(handles) - texts = list(texts) - HEIGHT = self._approx_text_height() - left = 0.5 + label_prop = dict(verticalalignment='baseline', + horizontalalignment='left', + fontproperties=self.prop, + ) - ret = [] # the returned legend lines + labelboxes = [] - # we need to pad the text with empties for the numpoints=1 - # centered marker proxy + for l in labels: + textbox = TextArea(l, textprops=label_prop) + text_list.append(textbox._text) + labelboxes.append(textbox) - for handle, label in safezip(handles, texts): + handleboxes = [] + + + # The approximate height and descent of text. These values are + # only used for plotting the legend handle. + height = self._approx_text_height() * 0.6 + descent = 0. #height/6. + + # each handle needs to be drawn inside a box of + # (x, y, w, h) = (0, -descent, width, height). + # And their corrdinates should be given in the display coordinates. + + # The transformation of each handle will be automatically set + # to self.get_trasnform(). If the artist does not uses its + # default trasnform (eg, Collections), you need to + # manually set their transform to the self.get_transform(). + + for handle in handles: if isinstance(handle, RegularPolyCollection): npoints = self.scatterpoints else: npoints = self.numpoints if npoints > 1: - xdata = np.linspace(left, left + self.handlelen, npoints) + # we put some pad here to compensate the size of the + # marker + xdata = np.linspace(0.3*self.fontsize, + (self.handlelength-0.3)*self.fontsize, + npoints) xdata_marker = xdata elif npoints == 1: - xdata = np.linspace(left, left + self.handlelen, 2) - xdata_marker = [left + 0.5*self.handlelen] + xdata = np.linspace(0, self.handlelength, 2) + xdata_marker = [0.5*self.handlelength*self.fontsize] - x, y = label.get_position() - x -= self.handlelen + self.handletextsep if isinstance(handle, Line2D): - ydata = (y-HEIGHT/2)*np.ones(xdata.shape, float) + ydata = ((height-descent)/2.)*np.ones(xdata.shape, float) legline = Line2D(xdata, ydata) legline.update_from(handle) @@ -288,8 +414,9 @@ legline.set_clip_path(None) legline.set_drawstyle('default') legline.set_marker('None') - ret.append(legline) + handle_list.append(legline) + legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)]) legline_marker.update_from(handle) self._set_artist_props(legline_marker) @@ -302,16 +429,17 @@ legline._legmarker = legline_marker elif isinstance(handle, Patch): - p = Rectangle(xy=(min(xdata), y-3/4*HEIGHT), - width = self.handlelen, height=HEIGHT/2, + p = Rectangle(xy=(0, -0.*descent), + width = self.handlelength*self.fontsize, + height=0.*descent+(height-descent)*.9, ) p.update_from(handle) self._set_artist_props(p) p.set_clip_box(None) p.set_clip_path(None) - ret.append(p) + handle_list.append(p) elif isinstance(handle, LineCollection): - ydata = (y-HEIGHT/2)*np.ones(xdata.shape, float) + ydata = ((height-descent)/2.)*np.ones(xdata.shape, float) legline = Line2D(xdata, ydata) self._set_artist_props(legline) legline.set_clip_box(None) @@ -322,13 +450,13 @@ legline.set_color(color) legline.set_linewidth(lw) legline.set_dashes(dashes) - ret.append(legline) + handle_list.append(legline) elif isinstance(handle, RegularPolyCollection): - # the ydata values set here have no effects as it will - # be updated in the _update_positions() method. - ydata = (y-HEIGHT/2)*np.ones(np.asarray(xdata_marker).shape, float) + #ydata = self._scatteryoffsets + ydata = height*self._scatteryoffsets + size_max, size_min = max(handle.get_sizes()),\ min(handle.get_sizes()) # we may need to scale these sizes by "markerscale" @@ -338,32 +466,86 @@ sizes = [.5*(size_max+size_min), size_max, size_min] else: - sizes = size_max*np.linspace(0,1,self.scatterpoints)+size_min - + sizes = (size_max-size_min)*np.linspace(0,1,self.scatterpoints)+size_min + p = type(handle)(handle.get_numsides(), rotation=handle.get_rotation(), sizes=sizes, offsets=zip(xdata_marker,ydata), - transOffset=self.get_transform()) - + transOffset=self.get_transform(), + ) + p.update_from(handle) p.set_figure(self.figure) p.set_clip_box(None) p.set_clip_path(None) - ret.append(p) + handle_list.append(p) else: - ret.append(None) + handle_list.append(None) - return ret + handlebox = DrawingArea(width=self.handlelength*self.fontsize, + height=height, + xdescent=0., ydescent=descent) + handle = handle_list[-1] + handlebox.add_artist(handle) + if hasattr(handle, "_legmarker"): + handlebox.add_artist(handle._legmarker) + handleboxes.append(handlebox) + + + # We calculate number of lows in each column. The first + # (num_largecol) columns will have (nrows+1) rows, and remaing + # (num_smallcol) columns will have (nrows) rows. + nrows, num_largecol = divmod(len(handleboxes), self._ncol) + num_smallcol = self._ncol-num_largecol + + # starting index of each column and number of rows in it. + largecol = zip(range(0, num_largecol*(nrows+1), (nrows+1)), + [nrows+1] * num_largecol) + smallcol = zip(range(num_largecol*(nrows+1), len(handleboxes), nrows), + [nrows] * num_smallcol) + + handle_label = zip(handleboxes, labelboxes) + columnbox = [] + for i0, di in largecol+smallcol: + # pack handleBox and labelBox into itemBox + itemBoxes = [HPacker(pad=0, + sep=self.handletextpad*self.fontsize, + children=[h, t], align="baseline") + for h, t in handle_label[i0:i0+di]] + + # pack columnBox + columnbox.append(VPacker(pad=0, + sep=self.labelspacing*self.fontsize, + align="baseline", + children=itemBoxes)) + + if self._mode == "expand": + mode = "expand" + else: + mode = "fixed" + + sep = self.columnspacing*self.fontsize + + self._legend_box = HPacker(pad=self.borderpad*self.fontsize, + sep=sep, align="baseline", + mode=mode, + children=columnbox) + + self.texts = text_list + self.legendHandles = handle_list + + def _auto_legend_data(self): - """ Returns list of vertices and extents covered by the plot. + """ + 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 + display-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 @@ -377,24 +559,21 @@ bboxes = [] lines = [] - inverse_transform = ax.transAxes.inverted() - for handle in ax.lines: assert isinstance(handle, Line2D) path = handle.get_path() trans = handle.get_transform() tpath = trans.transform_path(path) - apath = inverse_transform.transform_path(tpath) - lines.append(apath) + lines.append(tpath) for handle in ax.patches: assert isinstance(handle, Patch) if isinstance(handle, Rectangle): - transform = handle.get_data_transform() + inverse_transform + transform = handle.get_data_transform() bboxes.append(handle.get_bbox().transformed(transform)) else: - transform = handle.get_transform() + inverse_transform + transform = handle.get_transform() bboxes.append(handle.get_path().get_extents(transform)) return [vertices, bboxes, lines] @@ -404,9 +583,10 @@ self._drawFrame = b def get_children(self): + 'return a list of child artists' children = [] - children.extend(self.legendHandles) - children.extend(self.texts) + if self._legend_box: + children.append(self._legend_box) return children def get_frame(self): @@ -425,51 +605,61 @@ 'return a list of text.Text instance in the legend' return silent_list('Text', self.texts) - def _get_texts(self, labels, left, upper): + def get_window_extent(self): + 'return a extent of the the legend' + return self.legendPatch.get_window_extent() - # 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 + def _get_anchored_bbox(self, loc, bbox, parentbbox): + """ + Place the *bbox* inside the *parentbbox* according to a given + location code. Return the (x,y) coordinate of the bbox. - return ret + - loc: a location code in range(1, 11). + This corresponds to the possible values for self._loc, excluding "best". + - bbox: bbox to be placed, display coodinate units. + - parentbbox: a parent box which will contain the bbox. In + display coordinates. + """ + assert loc in range(1,11) # called only internally - def get_window_extent(self): - return self.legendPatch.get_window_extent() + BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) + anchor_coefs={UR:"NE", + UL:"NW", + LL:"SW", + LR:"SE", + R:"E", + CL:"W", + CR:"E", + LC:"S", + UC:"N", + C:"C"} + + c = anchor_coefs[loc] - def _offset(self, ox, oy): - 'Move all the artists by ox,oy (axes coords)' - self._offsetTransform.clear().translate(ox, oy) + container = parentbbox.padded(-(self.borderaxespad) * self.fontsize) + anchored_box = bbox.anchored(c, container=container) + return anchored_box.x0, anchored_box.y0 + def _find_best_position(self, width, height, consider=None): - """Determine the best location to place the legend. + """ + 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. + lower-left corner of the legend. All are display coords. """ assert self.isaxes # should always hold because function is only called internally verts, bboxes, lines = self._auto_legend_data() - consider = [self._loc_to_axes_coords(x, width, height) for x in range(1, len(self.codes))] + bbox = Bbox.from_bounds(0, 0, width, height) + consider = [self._get_anchored_bbox(x, bbox, self.parent.bbox) for x in range(1, len(self.codes))] - tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y() + #tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y() candidates = [] for l, b in consider: @@ -481,15 +671,16 @@ if line.intersects_bbox(legendBox): badness += 1 - ox, oy = l-tx, b-ty + ox, oy = l, b if badness == 0: return ox, oy - candidates.append((badness, (ox, oy))) + candidates.append((badness, (l, b))) # 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. + # NOTE: list.sort() is stable.But leave as it is for now. -JJL minCandidate = candidates[0] for candidate in candidates: if candidate[0] < minCandidate[0]: @@ -499,103 +690,3 @@ return ox, oy - - def _loc_to_axes_coords(self, loc, width, height): - """Convert a location code to axes coordinates. - - - loc: a location code in range(1, 11). - This corresponds to the possible values for self._loc, excluding "best". - - - width, height: the final size of the legend, axes units. - """ - assert loc in range(1,11) # called only internally - - BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) - - if loc in (UL, LL, CL): # left - x = self.axespad - elif loc in (UR, LR, CR, R): # right - x = 1.0 - (width + self.axespad) - elif loc in (LC, UC, C): # center x - x = (0.5 - width/2.0) - - if loc in (UR, UL, UC): # upper - y = 1.0 - (height + self.axespad) - elif loc in (LL, LR, LC): # lower - y = self.axespad - elif loc in (CL, CR, C, R): # center y - y = (0.5 - height/2.0) - - return x,y - - - 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 = bbox.inverse_transformed(self.get_transform()) - return bboxa.bounds - - hpos = [] - for t, tabove in safezip(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 safezip(self.legendHandles, hpos): - y,h = tup - if isinstance(handle, Line2D): - ydata = y*np.ones(handle.get_xdata().shape, float) - handle.set_ydata(ydata+h/2.) - if hasattr(handle, '_legmarker'): - handle._legmarker.set_ydata(ydata+h/2.) - elif isinstance(handle, Rectangle): - handle.set_y(y+1/4*h) - handle.set_height(h/2) - elif isinstance(handle,RegularPolyCollection): - offsets = handle.get_offsets() - offsets[:,1] = y+h*self._scatteryoffsets - handle.set_offsets(offsets) - - # Set the data for the legend patch - bbox = self._get_handle_text_bbox(renderer) - - if self.pad: - bbox = bbox.expanded(1 + self.pad, 1 + self.pad) - else: - bbox = bbox.transformed(self.get_transform()) - bbox = bbox.padded(self.borderpad*self.fontsize) - bbox = bbox.inverse_transformed(self.get_transform()) - l, b, w, h = bbox.bounds - - self.legendPatch.set_bounds(l, b, w, h) - - 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, oy = x-xo, y-yo - elif self._loc == 0: # "best" - ox, oy = self._find_best_position(w, h) - else: - x, y = self._loc_to_axes_coords(self._loc, w, h) - ox, oy = x-l, y-b - - self._offset(ox, oy) - -#artist.kwdocd['Legend'] = kwdoc(Legend) Added: trunk/matplotlib/lib/matplotlib/offsetbox.py =================================================================== --- trunk/matplotlib/lib/matplotlib/offsetbox.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/offsetbox.py 2008-12-01 14:51:35 UTC (rev 6461) @@ -0,0 +1,532 @@ +""" +The OffsetBox is a simple container artist. The child artist are meant +to be drawn at a relative position to its parent. The [VH]Packer, +DrawingArea and TextArea are derived from the OffsetBox. + +The [VH]Packer automatically adjust the relative postisions of their +children, which should be instances of the OffsetBox. This is used to +align similar artists together, e.g., in legend. + +The DrawingArea can contain any Artist as a child. The +DrawingArea has a fixed width and height. The position of children +relative to the parent is fixed. The TextArea is contains a single +Text instance. The width and height of the TextArea instance is the +width and height of the its child text. +""" + + +import matplotlib.transforms as mtransforms +import matplotlib.artist as martist +import matplotlib.text as mtext +import numpy as np + +from matplotlib.patches import bbox_artist as mbbox_artist +DEBUG=False +# for debuging use +def bbox_artist(*kl, **kw): + if DEBUG: + mbbox_artist(*kl, **kw) + + +# _get_packed_offsets() and _get_aligned_offsets() are coded assuming +# that we are packing boxes horizontally. But same function will be +# used with vertical packing. + +def _get_packed_offsets(wd_list, total, sep, mode="fixed"): + """ + Geiven a list of (width, xdescent) of each boxes, calculate the + total width and the x-offset positions of each items according to + *mode*. xdescent is analagous to the usual descent, but along the + x-direction. xdescent values are currently ignored. + + *wd_list* : list of (width, xdescent) of boxes to be packed. + *sep* : spacing between boxes + *total* : Intended total length. None if not used. + *mode* : packing mode. 'fixed', 'expand', or 'equal'. + """ + + w_list, d_list = zip(*wd_list) + # d_list is currently not used. + + if mode == "fixed": + offsets_ = np.add.accumulate([0]+[w + sep for w in w_list]) + offsets = offsets_[:-1] + + if total is None: + total = offsets_[-1] - sep + + return total, offsets + + elif mode == "expand": + sep = (total - sum(w_list))/(len(w_list)-1.) + offsets_ = np.add.accumulate([0]+[w + sep for w in w_list]) + offsets = offsets_[:-1] + + return total, offsets + + elif mode == "equal": + maxh = max(w_list) + if total is None: + total = (maxh+sep)*len(w_list) + else: + sep = float(total)/(len(w_list)) - maxh + + offsets = np.array([(maxh+sep)*i for i in range(len(w_list))]) + + return total, offsets + + else: + raise ValueError("Unknown mode : %s" % (mode,)) + + +def _get_aligned_offsets(hd_list, height, align="baseline"): + """ + Geiven a list of (height, descent) of each boxes, align the boxes + with *align* and calculate the y-offsets of each boxes. + total width and the offset positions of each items according to + *mode*. xdescent is analagous to the usual descent, but along the + x-direction. xdescent values are currently ignored. + + *hd_list* : list of (width, xdescent) of boxes to be aligned. + *sep* : spacing between boxes + *height* : Intended total length. None if not used. + *align* : align mode. 'baseline', 'top', 'bottom', or 'center'. + """ + + if height is None: + height = max([h for h, d in hd_list]) + + if align == "baseline": + height_descent = max([h-d for h, d in hd_list]) + descent = max([d for h, d in hd_list]) + height = height_descent + descent + offsets = [0. for h, d in hd_list] + elif align in ["left","top"]: + descent=0. + offsets = [d for h, d in hd_list] + elif align in ["right","bottom"]: + descent=0. + offsets = [height-h+d for h, d in hd_list] + elif align == "center": + descent=0. + offsets = [(height-h)*.5+d for h, d in hd_list] + else: + raise ValueError("Unknown Align mode : %s" % (align,)) + + return height, descent, offsets + + + +class OffsetBox(martist.Artist): + """ + The OffsetBox is a simple container artist. The child artist are meant + to be drawn at a relative position to its parent. + """ + def __init__(self, *kl, **kw): + + super(OffsetBox, self).__init__(*kl, **kw) + + self._children = [] + self._offset = (0, 0) + + def set_figure(self, fig): + """ + Set the figure + + accepts a class:`~matplotlib.figure.Figure` instance + """ + martist.Artist.set_figure(self, fig) + for c in self.get_children(): + c.set_figure(fig) + + def set_offset(self, xy): + """ + Set the offset + + accepts x, y, tuple, or a callable object. + """ + self._offset = xy + + def get_offset(self, width, height, xdescent, ydescent): + """ + Get the offset + + accepts extent of the box + """ + if callable(self._offset): + return self._offset(width, height, xdescent, ydescent) + else: + return self._offset + + def set_width(self, width): + """ + Set the width + + accepts float + """ + self._width = width + + def set_height(self, height): + """ + Set the height + + accepts float + """ + self._height = height + + def get_children(self): + """ + Return a list of artists it contains. + """ + return self._children + + def get_extent_offsets(self, renderer): + raise Exception("") + + def get_extent(self, renderer): + """ + Return with, height, xdescent, ydescent of box + """ + w, h, xd, yd, offsets = self.get_extent_offsets(renderer) + return w, h, xd, yd + + def get_window_extent(self, renderer): + ''' + get the bounding box in display space. + ''' + w, h, xd, yd, offsets = self.get_extent_offsets(renderer) + px, py = self.get_offset(w, h, xd, yd) + return mtransforms.Bbox.from_bounds(px-xd, py-yd, w, h) + + def draw(self, renderer): + """ + Update the location of children if necessary and draw them + to the given *renderer*. + """ + + width, height, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) + + px, py = self.get_offset(width, height, xdescent, ydescent) + + for c, (ox, oy) in zip(self.get_children(), offsets): + c.set_offset((px+ox, py+oy)) + c.draw(renderer) + + bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + + +class VPacker(OffsetBox): + """ + The VPacker has its children packed vertically. It automatically + adjust the relative postisions of children in the drawing time. + """ + def __init__(self, pad=None, sep=None, width=None, height=None, + align="baseline", mode="fixed", + children=None): + """ + *pad* : boundary pad + *sep* : spacing between items + *width*, *height* : width and height of the container box. + calculated if None. + *align* : alignment of boxes + *mode* : packing mode + """ + super(VPacker, self).__init__() + + self._height = height + self._width = width + self._align = align + self._sep = sep + self._pad = pad + self._mode = mode + + self._children = children + + + def get_extent_offsets(self, renderer): + """ + update offset of childrens and return the extents of the box + """ + + whd_list = [c.get_extent(renderer) for c in self.get_children()] + whd_list = [(w, h, xd, (h-yd)) for w, h, xd, yd in whd_list] + + + wd_list = [(w, xd) for w, h, xd, yd in whd_list] + width, xdescent, xoffsets = _get_aligned_offsets(wd_list, + self._width, + self._align) + + pack_list = [(h, yd) for w,h,xd,yd in whd_list] + height, yoffsets_ = _get_packed_offsets(pack_list, self._height, + self._sep, self._mode) + + yoffsets = yoffsets_ + [yd for w,h,xd,yd in whd_list] + ydescent = height - yoffsets[0] + yoffsets = height - yoffsets + + #w, h, xd, h_yd = whd_list[-1] + yoffsets = yoffsets - ydescent + + return width + 2*self._pad, height + 2*self._pad, \ + xdescent+self._pad, ydescent+self._pad, \ + zip(xoffsets, yoffsets) + + + +class HPacker(OffsetBox): + """ + The HPacker has its children packed horizontally. It automatically + adjust the relative postisions of children in the drawing time. + """ + def __init__(self, pad=None, width=None, height=None, sep=None, + align="baseline", mode="fixed", + children=None): + """ + *pad* : boundary pad + *sep* : spacing between items + *width*, *height* : width and height of the container box. + calculated if None. + *align* : alignment of boxes + *mode* : packing mode + """ + super(HPacker, self).__init__() + + self._height = height + self._width = width + self._align = align + + self._sep = sep + self._pad = pad + self._mode = mode + + self._children = children + + + def get_extent_offsets(self, renderer): + """ + update offset of childrens and return the extents of the box + """ + + whd_list = [c.get_extent(renderer) for c in self.get_children()] + + if self._height is None: + height_descent = max([h-yd for w,h,xd,yd in whd_list]) + ydescent = max([yd for w,h,xd,yd in whd_list]) + height = height_descent + ydescent + else: + height = self._height - 2*self._pad # width w/o pad + + hd_list = [(h, yd) for w, h, xd, yd in whd_list] + height, ydescent, yoffsets = _get_aligned_offsets(hd_list, + self._height, + self._align) + + + pack_list = [(w, xd) for w,h,xd,yd in whd_list] + width, xoffsets_ = _get_packed_offsets(pack_list, self._width, + self._sep, self._mode) + + xoffsets = xoffsets_ + [xd for w,h,xd,yd in whd_list] + + xdescent=whd_list[0][2] + xoffsets = xoffsets - xdescent + + return width + 2*self._pad, height + 2*self._pad, \ + xdescent + self._pad, ydescent + self._pad, \ + zip(xoffsets, yoffsets) + + + +class DrawingArea(OffsetBox): + """ + The DrawingArea can contain any Artist as a child. The DrawingArea + has a fixed width and height. The position of children relative to + the parent is fixed. + """ + + def __init__(self, width, height, xdescent=0., + ydescent=0., clip=True): + """ + *width*, *height* : width and height of the container box. + *xdescent*, *ydescent* : descent of the box in x- and y-direction. + """ + + super(DrawingArea, self).__init__() + + self.width = width + self.height = height + self.xdescent = xdescent + self.ydescent = ydescent + + self.offset_transform = mtransforms.Affine2D() + self.offset_transform.clear() + self.offset_transform.translate(0, 0) + + + def get_transform(self): + """ + Return the :class:`~matplotlib.transforms.Transform` applied + to the children + """ + return self.offset_transform + + def set_transform(self, t): + """ + set_transform is ignored. + """ + pass + + + def set_offset(self, xy): + """ + set offset of the container. + + Accept : tuple of x,y cooridnate in disokay units. + """ + self._offset = xy + + self.offset_transform.clear() + self.offset_transform.translate(xy[0], xy[1]) + + + def get_offset(self): + """ + return offset of the container. + """ + return self._offset + + + def get_window_extent(self, renderer): + ''' + get the bounding box in display space. + ''' + w, h, xd, yd = self.get_extent(renderer) + ox, oy = self.get_offset() #w, h, xd, yd) + return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) + + + def get_extent(self, renderer): + """ + Return with, height, xdescent, ydescent of box + """ + return self.width, self.height, self.xdescent, self.ydescent + + + + def add_artist(self, a): + 'Add any :class:`~matplotlib.artist.Artist` to the container box' + self._children.append(a) + a.set_transform(self.get_transform()) + + + def draw(self, renderer): + """ + Draw the children + """ + + for c in self._children: + c.draw(renderer) + + bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + + +class TextArea(OffsetBox): + """ + The TextArea is contains a single Text instance. The text is + placed at (0,0) with baseline+left alignment. The width and height + of the TextArea instance is the width and height of the its child + text. + """ + + + + def __init__(self, s, textprops=None, **kw): + """ + *s* : a string to be displayer. + *trnaspose* : transformation matrrix + """ + if textprops is None: + textprops = {} + + if not textprops.has_key("va"): + textprops["va"]="baseline" + + self._text = mtext.Text(0, 0, s, **textprops) + + OffsetBox.__init__(self) + + self._children = [self._text] + + + self.offset_transform = mtransforms.Affine2D() + self.offset_transform.clear() + self.offset_transform.translate(0, 0) + self._text.set_transform(self.offset_transform) + + + def set_transform(self, t): + """ + set_transform is ignored. + """ + pass + + + def set_offset(self, xy): + """ + set offset of the container. + + Accept : tuple of x,y cooridnate in disokay units. + """ + self._offset = xy + + self.offset_transform.clear() + self.offset_transform.translate(xy[0], xy[1]) + + + def get_offset(self): + """ + return offset of the container. + """ + return self._offset + + + def get_window_extent(self, renderer): + ''' + get the bounding box in display space. + ''' + w, h, xd, yd = self.get_extent(renderer) + ox, oy = self.get_offset() #w, h, xd, yd) + return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) + + + def get_extent(self, renderer): + ismath = self._text.is_math_text(self._text._text) + _, h_, d_ = renderer.get_text_width_height_descent( + "lp", self._text._fontproperties, ismath=False) + + bbox, info = self._text._get_layout(renderer) + w, h = bbox.width, bbox.height + line = info[0][0] # first line + + _, hh, dd = renderer.get_text_width_height_descent( + line, self._text._fontproperties, ismath=ismath) + d = h-(hh-dd) # the baseline of the first line + + # for multiple lines, h or d may greater than h_ or d_. + h_d = max(h_ - d_, h-d) + d = max(d, d_) + h = h_d + d + + return w, h, 0., d + + + def draw(self, renderer): + """ + Draw the children + """ + + self._text.draw(renderer) + + bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + Modified: trunk/matplotlib/lib/matplotlib/rcsetup.py =================================================================== --- trunk/matplotlib/lib/matplotlib/rcsetup.py 2008-12-01 14:06:49 UTC (rev 6460) +++ trunk/matplotlib/lib/matplotlib/rcsetup.py 2008-12-01 14:51:35 UTC (rev 6461) @@ -422,7 +422,7 @@ 'legend.numpoints' : [2, validate_int], # the number of points in the legend line 'legend.fontsize' : ['large', validate_fontsize], 'legend.pad' : [0, validate_float], # was 0.2, deprecated; the fractional whitespace inside the legend border - 'legend.borderpad' : [0.5, validate_float], # units are fontsize + 'legend.borderpad' : [0.4, validate_float], # units are fontsize 'legend.markerscale' : [1.0, validate_float], # the relative size of legend markers vs. original # the following dimensions are in axes coords @@ -433,6 +433,25 @@ 'legend.shadow' : [False, validate_bool], + 'legend.labelspacing' : [0.5, validate_float], # the vertical space between the legend entries + 'legend.handlelength' : [2., validate_float], # the length of the legend lines + 'legend.handletextpad' : [.8, validate_float], # the space between the legend line and legend text + 'legend.borderaxespad' : [0.5, validate_float], # the border between the axes and legend edge + 'legend.columnspacing' : [2., validate_float], # the border between the axes and legend edge + + + 'legend.markerscale' : [1.0, validate_float], # the relative size of legend markers vs. original + + # the following dimensions are in axes coords + 'legend.labelsep' : [0.010, validate_float], # the vertical space between the legend entries + 'legend.handlelen' : [0.05, validate_float], # the length of the legend lines + 'legend.handletextsep' : [0.02, validate_float], # the space between the legend line and legend text + 'legend.axespad' : [0.5, validate_float], # the border between the axes and legend edge + 'legend.shadow' : [False, validate_bool], + + + + # tick properties 'xtick.major.size' : [4, validate_float], # major xtick size in points 'xtick.minor.size' : [2, validate_float], # minor xtick size in points This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |