Hi folks,
I recently required a multi-column legend for a matplotlib graph. I
hacked up the Legend class to support this. I figured this might be
useful to others, so I'm attaching a patch in the hopes that someone
who is a regular contributor will review it and check it in.
Disclaimer: I spent an hour learning python for the express purpose of
making these graphs. That's the extent of my python experience so
some things might have been done the long way around.
It adds two parameters to the legend class:
The first 'rowspercolumn' specifies how often to start a new
column. If it is -1 then the current single-column behaviour is
reproduced. I set this to -1 in my matplotlibrc file, and pass a
specific number to the legend command when I want a multi-column
legend.
The second 'columnsep' specifies the distance between the right side
of one column's label and the left side of the line of the next
column.
Columns are sized horizontally to fit the largest label in the
column. I've tested it "extensively" on my case, with various
combinations of columns and legend entries. It seems to be stable. But
"extensively" is probably not a huge amount, honestly, and I'm new to
matplotlib so I may not have covered all the cases.
Cheers,
Colin
p.s. the patch is against the MacOS X binary distro which is at
0.87.4.
*** legend-orig.py Fri Aug 11 18:15:29 2006
--- legend.py Sat Aug 12 12:27:39 2006
***************
*** 36,41 ****
--- 36,42 ----
from text import Text
from transforms import Bbox, Point, Value, get_bbox_transform, bbox_all,\
unit_bbox, inverse_transform_bbox, lbwh_to_bbox
+ from math import ceil
***************
*** 120,153 ****
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
fontprop = 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]
--- 121,158 ----
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
! rowspercolumn = None, # the number of rows after which a new
column will be started
! columnsep = None, # additional space between columns
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
fontprop = 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
! rowspercolumn = Infinite # the number of rows after which a new column
will be started (Inf=-1)
! 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
+ columnsep = 0.04 # additional space between columns
+
"""
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, rowspercolumn, columnsep]
! propnames=['numpoints', 'pad', 'markerscale', 'labelsep',
'handlelen', 'handletextsep', 'axespad', 'shadow', 'isaxes', 'rowspercolumn',
'columnsep']
for name, value in zip(propnames,proplist):
if value is None:
value=rcParams["legend."+name]
***************
*** 523,550 ****
bbox = text.get_window_extent(renderer)
bboxa = inverse_transform_bbox(self._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)
--- 528,603 ----
bbox = text.get_window_extent(renderer)
bboxa = inverse_transform_bbox(self._transform, bbox)
return bboxa.get_bounds()
+
+ def get_twidth(text): # get text width in regular coords
+ return get_tbounds(text)[2]
hpos = []
! if (self.rowspercolumn == -1):
! colcount=len(self.texts)
! cols = 1
! else:
! cols=ceil(len(self.texts)/self.rowspercolumn)
! colcount=self.rowspercolumn
! topy = self.texts[0].get_position()[1] # remember top of first item
! toph = get_tbounds(self.texts[0])[3] # remember height of first item
! topb = get_tbounds(self.texts[0])[1] # remember bottom of first item
! lastcol=-1
! widest=0
! tabovelist=self.texts[:] # zip texts with the prev texts.
process all but first text.
! tabovelist.insert(0,self.texts[0]) # so 2bl the 1st item in the 2nd
list and skip 1st loop.
! tabovelist.pop # gives tlist=[A B C D E F]
abovetlist=[A A B C D E]
! for col in range(cols):
! for t, tabove in
zip(self.texts[int(col*colcount):int((col+1)*colcount)],
tabovelist[int(col*colcount):int((col+1)*colcount)]):
! if ((lastcol == -1) and (widest==0)):
! # skip first as we are placing relative to it
! # but capture its width for column spacing
! w2 = get_twidth(t)
! if (widest < w2):
! widest = w2
! lastcol=0
! continue
! x,y = t.get_position()
! l,b,w,h = get_tbounds(tabove)
! if ((lastcol != col) and (lastcol != -1)):
! # new column so hack up so tabove looks like first item
translated right
! x,y = (l + widest + self.columnsep + self.handlelen +
self.handletextsep, topy)
! b = topb
! h = toph
! else:
! # continue down from previous label
! x = l
! b -= self.labelsep
! h += 2*self.labelsep
! y = b-0.1*h
! hpos.append( (b,h) )
! t.set_position( (x, y) )
! if (lastcol != col):
! # reset width tracking for new column
! widest=0
! lastcol=col
! w2 = get_twidth(t)
! if (widest < w2):
! widest = w2
!
! # make hpos for the last line
l,b,w,h = get_tbounds(self.texts[-1])
b -= self.labelsep
h += 2*self.labelsep
hpos.append( (b,h) )
+ # move lines to correspond with labels
+ for line, label in zip(self.get_lines(), self.get_texts()):
+ l,b,w,h = get_tbounds(label)
+ x = l - self.handlelen - self.handletextsep
+ xdata = linspace(x, x + self.handlelen, self.numpoints)
+ ydata = b+h/2*ones(self._xdata.shape, Float)
+ line.set_xdata(xdata)
+ line.set_ydata(ydata)
+
for handle, tup in zip(self.legendHandles, hpos):
! if isinstance(handle, Rectangle):
! y,h = tup
handle.set_y(y+1/4*h)
handle.set_height(h/2)
-------------------------------------------------------------------------
Using Tomcat but need to do more? Need to support web services, security?
Get stuff done quickly with pre-integrated technology to make your job easier
Download IBM WebSphere Application Server v.1.0.1 based on Apache Geronimo
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=120709&bid=263057&dat=121642
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel