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

Reply via email to