|
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.
|