Finished off sizing logic to go with row and column
authorandy_robinson
Mon, 21 Apr 2003 22:27:49 +0000
changeset 1912 c8509682e3e0
parent 1911 4e671c313a81
child 1913 a6169f866f61
Finished off sizing logic to go with row and column spanning; added a substantial set of examples.
reportlab/platypus/tables.py
reportlab/test/test_table_layout.py
--- a/reportlab/platypus/tables.py	Thu Apr 17 23:37:39 2003 +0000
+++ b/reportlab/platypus/tables.py	Mon Apr 21 22:27:49 2003 +0000
@@ -1,8 +1,8 @@
 #copyright ReportLab Inc. 2000
 #see license.txt for license details
 #history http://cvs.sourceforge.net/cgi-bin/cvsweb.cgi/reportlab/platypus/tables.py?cvsroot=reportlab
-#$Header: /tmp/reportlab/reportlab/platypus/tables.py,v 1.63 2003/04/05 23:46:42 andy_robinson Exp $
-__version__=''' $Id: tables.py,v 1.63 2003/04/05 23:46:42 andy_robinson Exp $ '''
+#$Header: /tmp/reportlab/reportlab/platypus/tables.py,v 1.64 2003/04/21 22:27:47 andy_robinson Exp $
+__version__=''' $Id: tables.py,v 1.64 2003/04/21 22:27:47 andy_robinson Exp $ '''
 __doc__="""
 Tables are created by passing the constructor a tuple of column widths, a tuple of row heights and the data in
 row order. Drawing of the table can be controlled by using a TableStyle instance. This allows control of the
@@ -234,32 +234,50 @@
         return w, t - V[0].getSpaceBefore()-V[-1].getSpaceAfter()
 
     def _calc_width(self):
-
-        W = self._argW
-
+        #comments added by Andy to Robin's slightly
+        #terse variable names
+        W = self._argW  #widths array
+        #print 'widths array = %s' % str(self._colWidths)
         canv = getattr(self,'canv',None)
         saved = None
 
-        if None in W:
+        if None in W:  #some column widths are not given
+            if self._spanCmds:
+                colspans = self._colSpannedCells
+            else:
+                colspans = {}
+##            k = colspans.keys()
+##            k.sort()
+##            print 'the following cells are part of spanned ranges: %s' % k
             W = W[:]
             self._colWidths = W
             while None in W:
-                j = W.index(None)
+                j = W.index(None) #find first unspecified column
+                #print 'sizing column %d' % j
                 f = lambda x,j=j: operator.getitem(x,j)
-                V = map(f,self._cellvalues)
-                S = map(f,self._cellStyles)
+                V = map(f,self._cellvalues)  #values for this column
+                S = map(f,self._cellStyles)  #styles for this column
                 w = 0
                 i = 0
+                
                 for v, s in map(None, V, S):
+                    #if the current cell is part of a spanned region,
+                    #assume a zero size.
+                    if colspans.has_key((j, i)):
+                        #print 'sizing a spanned cell (%d, %d) with content "%s"' % (j, i, str(v))
+                        t = 0.0
+                    else:#work out size
+                        t = type(v)
+                        if t in _SeqTypes or isinstance(v,Flowable):
+                            raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width\n%s" % (v.identity(30),i,j,self.identity(30))
+                        elif t is not StringType: v = v is None and '' or str(v)
+                        v = string.split(v, "\n")
+                        t = s.leftPadding+s.rightPadding + max(map(lambda a, b=s.fontname,
+                                    c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v))
+                    if t>w: w = t   #record a new maximum
                     i = i + 1
-                    t = type(v)
-                    if t in _SeqTypes or isinstance(v,Flowable):
-                        raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width\n%s" % (v.identity(30),i,j,self.identity(30))
-                    elif t is not StringType: v = v is None and '' or str(v)
-                    v = string.split(v, "\n")
-                    t = s.leftPadding+s.rightPadding + max(map(lambda a, b=s.fontname,
-                                c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v))
-                    if t>w: w = t   #record a new maximum
+
+                #print 'max width for column %d is %0.2f' % (j, w)
                 W[j] = w
 
         width = 0
@@ -280,6 +298,13 @@
         canv = getattr(self,'canv',None)
         saved = None
 
+        #get a handy list of any cells which span rows.
+        #these should be ignored for sizing
+        if self._spanCmds:
+            spans = self._rowSpannedCells
+        else:
+            spans = {}
+
         if None in H:
             if canv: saved = canv._fontname, canv._fontsize, canv._leading
             H = H[:]    #make a copy as we'll change it
@@ -291,26 +316,30 @@
                 h = 0
                 j = 0
                 for v, s, w in map(None, V, S, W): # value, style, width (lengths must match)
+                    if spans.has_key((j, i)):
+                        t = 0.0  # don't count it, it's either occluded or unreliable
+                    else:
+                        t = type(v)
+                        if t in _SeqTypes or isinstance(v,Flowable):
+                            if not t in _SeqTypes: v = (v,)
+                            if w is None:
+                                raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width in\n%s" % (v[0].identity(30),i,j,self.identity(30))
+                            if canv: canv._fontname, canv._fontsize, canv._leading = s.fontname, s.fontsize, s.leading or 1.2*s.fontsize
+                            dW,t = self._listCellGeom(v,w,s)
+                            if canv: canv._fontname, canv._fontsize, canv._leading = saved
+                            #print "leftpadding, rightpadding", s.leftPadding, s.rightPadding
+                            dW = dW + s.leftPadding + s.rightPadding
+                            if not rl_config.allowTableBoundsErrors and dW>w:
+                                raise "LayoutError", "Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30))
+                        else:
+                            if t is not StringType:
+                                v = v is None and '' or str(v)
+                            v = string.split(v, "\n")
+                            t = s.leading*len(v)
+                        t = t+s.bottomPadding+s.topPadding
+                    if t>h: h = t   #record a new maximum
                     j = j + 1
-                    t = type(v)
-                    if t in _SeqTypes or isinstance(v,Flowable):
-                        if not t in _SeqTypes: v = (v,)
-                        if w is None:
-                            raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width in\n%s" % (v[0].identity(30),i,j,self.identity(30))
-                        if canv: canv._fontname, canv._fontsize, canv._leading = s.fontname, s.fontsize, s.leading or 1.2*s.fontsize
-                        dW,t = self._listCellGeom(v,w,s)
-                        if canv: canv._fontname, canv._fontsize, canv._leading = saved
-                        #print "leftpadding, rightpadding", s.leftPadding, s.rightPadding
-                        dW = dW + s.leftPadding + s.rightPadding
-                        if not rl_config.allowTableBoundsErrors and dW>w:
-                            raise "LayoutError", "Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30))
-                    else:
-                        if t is not StringType:
-                            v = v is None and '' or str(v)
-                        v = string.split(v, "\n")
-                        t = s.leading*len(v)
-                    t = t+s.bottomPadding+s.topPadding
-                    if t>h: h = t   #record a new maximum
+
                 H[i] = h
 
         height = self._height = reduce(operator.add, H, 0)
@@ -321,10 +350,25 @@
             self._rowpositions.append(height)
         assert abs(height)<1e-8, 'Internal height error'
 
-    def _calc(self):
+    def _calc(self, availWidth, availHeight):
         if hasattr(self,'_width'): return
 
+        #in some cases there are unsizable things in
+        #cells.  If so, apply a different algorithm
+        #and assign some withs in a dumb way.
+        #this CHANGES the widths array.
+        if None in self._colWidths:
+            if self._hasUnsizableElements():
+                self._calcPreliminaryWidths(availWidth)
+
+        # need to know which cells are part of spanned
+        # ranges, so _calc_height and _calc_width can ignore them
+        # in sizing
+        if self._spanCmds:
+            self._calcSpanRanges()
+            
         # calculate the full table height
+        #print 'during calc, self._colWidths=', self._colWidths
         self._calc_height()
 
         # if the width has already been calculated, don't calculate again
@@ -335,24 +379,110 @@
         # calculate the full table width
         self._calc_width()
 
+        
         if self._spanCmds:
+            #now work out the actual rect for each spanned cell
+            #from the underlying grid
             self._calcSpanRects()
-            
+
+    def _hasUnsizableElements(self, upToRow=None):
+        """Check for flowables in table cells and warn up front.
+
+        Allow a couple which we know are fixed size such as
+        images and graphics."""
+        bad = 0
+        if upToRow is None: upToRow = self._nrows
+        for row in range(min(self._nrows, upToRow)):
+            for col in range(self._ncols):
+                value = self._cellvalues[row][col]
+                if not self._canSize(value):
+                    bad = 1
+                    #raise Exception('Unsizable elements found at row %d column %d in table with content:\n %s' % (row, col, value))
+        return bad
+
+    def _canSize(self, thing):
+        "Can we work out the width quickly?"
+        if type(thing) in (ListType, TupleType):
+            for elem in thing:
+                if not self._canSize(elem):
+                    return 0
+            return 1
+        elif isinstance(thing, Flowable):
+            return 0  # must loosen this up
+        else: #string, number, None etc.
+            #anything else gets passed to str(...)
+            # so should be sizable
+            return 1
+
+    def _calcPreliminaryWidths(self, availWidth):
+        """Fallback algorithm for when main one fails.
 
-    def _calcSpanRects(self):
+        Where exact width info not given but things like
+        paragraphs might be present, do a preliminary scan
+        and assign some sensible values - just divide up
+        all unsizeable columns by the remaining space."""
+        verbose = 0
+        totalDefined = 0.0
+        numberUndefined = 0
+        for w in self._colWidths:
+            if w is None:
+                numberUndefined = numberUndefined + 1
+            else:
+                totalDefined = totalDefined + w
+        if verbose: print 'prelim width calculation.  %d columns, %d undefined width, %0.2f units remain' % (
+            self._ncols, numberUndefined, availWidth - totalDefined)
+
+        #check columnwise in each None column to see if they are sizable.
+        given = []
+        sizeable = []
+        unsizeable = []
+        for colNo in range(self._ncols):
+            if self._colWidths[colNo] is None:
+                siz = 1
+                for rowNo in range(self._nrows):
+                    value = self._cellvalues[rowNo][colNo]
+                    if not self._canSize(value):
+                        siz = 0
+                        break
+                if siz:
+                    sizeable.append(colNo)
+                else:
+                    unsizeable.append(colNo)
+            else:
+                given.append(colNo)
+        if len(given) == self._ncols:
+            return
+        if verbose: print 'predefined width:   ',given
+        if verbose: print 'uncomputable width: ',unsizeable
+        if verbose: print 'computable width:    ',sizeable
+
+        #how much width is left:
+        # on the next iteration we could size the sizeable ones, for now I'll just
+        # divide up the space
+        newColWidths = list(self._colWidths)
+        guessColWidth = (availWidth - totalDefined) / (len(unsizeable)+len(sizeable))
+        assert guessColWidth >= 0, "table is too wide already, cannot choose a sane width for undefined columns"
+        if verbose: print 'assigning width %0.2f to all undefined columns' % guessColWidth
+        for colNo in sizeable:
+            newColWidths[colNo] = guessColWidth
+        for colNo in unsizeable:
+            newColWidths[colNo] = guessColWidth
+
+        self._colWidths = newColWidths
+        self._argW = newColWidths
+        if verbose: print 'new widths are:', self._colWidths
+        
+
+    def _calcSpanRanges(self):
         """Work out rects for tables which do row and column spanning.
 
-        This is a first try.  The idea is to do the ordinary sizing
-        first and then make two mappings:
-
+        This creates some mappings to let the later code determine
+        if a cell is part of a "spanned" range.
         self._spanRanges shows the 'coords' in integers of each
         'cell range', or None if it was clobbered:
           (col, row) -> (col0, row0, col1, row1)
-        self._spanRects shows the real coords for drawing:
-          (col, row) -> (x, y, width, height)
-        
-        for each cell.  Any cell which 'does not exist' as another
-        has spanned over it will get a None entry on the right
+
+        Any cell not in the key is not part of a spanned region
         """
         spanRanges = {}
         for row in range(self._nrows):
@@ -382,16 +512,51 @@
 
             # set the main entry            
             spanRanges[x0,y0] = (x0, y0, x1, y1)
+##            from pprint import pprint as pp
+##            pp(spanRanges)
         self._spanRanges = spanRanges
+
+        #now produce a "listing" of all cells which
+        #are part of a spanned region, so the normal
+        #sizing algorithm can not bother sizing such cells
+        colSpannedCells = {}
+        for (key, value) in spanRanges.items():
+            if value is None:
+                colSpannedCells[key] = 1
+            elif len(value) == 4:
+                if value[0] == value[2]:
+                    #not colspanned
+                    pass
+                else:
+                    colSpannedCells[key] = 1
+        self._colSpannedCells = colSpannedCells
+        #ditto for row-spanned ones.
+        rowSpannedCells = {}
+        for (key, value) in spanRanges.items():
+            if value is None:
+                rowSpannedCells[key] = 1
+            elif len(value) == 4:
+                if value[1] == value[3]:
+                    #not rowspanned
+                    pass
+                else:
+                    rowSpannedCells[key] = 1
+        self._rowSpannedCells = rowSpannedCells
         
-        # now make map 2.  This maps (col, row) to the actual
-        #rectangle to draw with x,y,width,height info
-##        print 'rowpositions = ', self._rowpositions
-##        print 'rowHeights = ', self._rowHeights
-##        print 'colpositions = ', self._colpositions
-##        print 'colWidths = ', self._colWidths
+
+    def _calcSpanRects(self):
+        """Work out rects for tables which do row and column spanning.
+
+        Based on self._spanRanges, which is already known,
+        and the widths which were given or previously calculated, 
+        self._spanRects shows the real coords for drawing:
+          (col, row) -> (x, y, width, height)
+        
+        for each cell.  Any cell which 'does not exist' as another
+        has spanned over it will get a None entry on the right
+        """
         spanRects = {}
-        for (coord, value) in spanRanges.items():
+        for (coord, value) in self._spanRanges.items():
             if value is None:
                 spanRects[coord] = None
             else:
@@ -405,11 +570,6 @@
                 
         self._spanRects = spanRects
             
-##        from pprint import pprint as pp
-##        print 'span ranges:'
-##        pp(spanRanges)
-##        print '\ncell rects:'
-##        pp(spanRects)        
 
     def setStyle(self, tblstyle):
         if type(tblstyle) is not TableStyleType:
@@ -529,7 +689,7 @@
         self._drawVLines((sc+1, sr), (ec+1, er), weight, color)
 
     def wrap(self, availWidth, availHeight):
-        self._calc()
+        self._calc(availWidth, availHeight)
         #nice and easy, since they are predetermined size
         self.availWidth = availWidth
         return (self._width, self._height)
@@ -665,7 +825,7 @@
         return [R0,R1]
 
     def split(self, availWidth, availHeight):
-        self._calc()
+        self._calc(availWidth, availHeight)
         if self.splitByRow:
             if self._width>availWidth: return []
             return self._splitRows(availHeight)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/reportlab/test/test_table_layout.py	Mon Apr 21 22:27:49 2003 +0000
@@ -0,0 +1,353 @@
+import operator, string
+
+from reportlab.platypus import *
+#from reportlab import rl_config
+from reportlab.lib.styles import PropertySet, getSampleStyleSheet, ParagraphStyle
+from reportlab.lib import colors
+from reportlab.platypus.paragraph import Paragraph
+#from reportlab.lib.utils import fp_str
+#from reportlab.pdfbase import pdfmetrics
+from reportlab.platypus.flowables import PageBreak
+
+
+import os
+
+from reportlab.test import unittest
+from reportlab.test.utils import makeSuiteForClasses
+
+
+from types import TupleType, ListType, StringType
+
+
+class TableTestCase(unittest.TestCase):
+
+
+    def getDataBlock(self):
+        "Helper - data for our spanned table"
+        return [
+            # two rows are for headers
+            ['Region','Product','Period',None,None,None,'Total'],
+            [None,None,'Q1','Q2','Q3','Q4',None],
+
+            # now for data
+            ['North','Spam',100,110,120,130,460],
+            ['North','Eggs',101,111,121,131,464],
+            ['North','Guinness',102,112,122,132,468],
+            
+            ['South','Spam',100,110,120,130,460],
+            ['South','Eggs',101,111,121,131,464],
+            ['South','Guinness',102,112,122,132,468],
+            ]
+
+    def test_document(self):
+
+        rowheights = (24, 16, 16, 16, 16)
+        rowheights2 = (24, 16, 16, 16, 30)
+        colwidths = (50, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32)
+        GRID_STYLE = TableStyle(
+            [('GRID', (0,0), (-1,-1), 0.25, colors.black),
+             ('ALIGN', (1,1), (-1,-1), 'RIGHT')]
+            )
+
+        styleSheet = getSampleStyleSheet()
+        styNormal = styleSheet['Normal']
+        styNormal.spaceBefore = 6
+        styNormal.spaceAfter = 6
+        
+        data = (
+            ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'),
+            ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89),
+            ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119),
+            ('Miscellaneous accessories', 0,0,0,0,0,0,1,0,0,0,2,13),
+            ('Hats', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843')
+            )
+        data2 = (
+            ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'),
+            ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89),
+            ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119),
+            ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13),
+            ('Hats\nLarge', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843')
+            )
+
+
+        data3 = (
+            ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'),
+            ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89),
+            ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119),
+            ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13),
+            (Paragraph("Let's <b>really mess things up with a <i>paragraph</i>",styNormal),
+                   893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843')
+            )
+
+        lst = []
+
+
+        lst.append(Paragraph("""Basics about column sizing and cell contents""", styleSheet['Heading1']))
+
+        t1 = Table(data, colwidths, rowheights)
+        t1.setStyle(GRID_STYLE)
+        lst.append(Paragraph("This is GRID_STYLE with explicit column widths.  Each cell contains a string or number\n", styleSheet['BodyText']))
+        lst.append(t1)
+        lst.append(Spacer(18,18))
+
+        t2 = Table(data, None, None)
+        t2.setStyle(GRID_STYLE)
+        lst.append(Paragraph("""This is GRID_STYLE with no size info. It
+                                does the sizes itself, measuring each text string
+                                and computing the space it needs.  If the text is
+                                too wide for the frame, the table will overflow
+                                as seen here.""",
+                             styNormal))
+        lst.append(t2)
+        lst.append(Spacer(18,18))
+
+        t3 = Table(data2, None, None)
+        t3.setStyle(GRID_STYLE)
+        lst.append(Paragraph("""This demonstrates the effect of adding text strings with
+        newlines to a cell. It breaks where you specify, and if rowHeights is None (i.e
+        automatic) then you'll see the effect. See bottom left cell.""",
+                             styNormal))
+        lst.append(t3)
+        lst.append(Spacer(18,18))
+
+        
+        colWidths = (None, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32)
+        t3 = Table(data3, colWidths, None)
+        t3.setStyle(GRID_STYLE)
+        lst.append(Paragraph("""This table does not specify the size of the first column,
+                                so should work out a sane one.  In this case the element
+                                at bottom left is a paragraph, which has no intrinsic size
+                                (the height and width are a function of each other).  So,
+                                it tots up the extra space in the frame and divides it
+                                between any such unsizeable columns.  As a result the
+                                table fills the width of the frame (except for the
+                                6 point padding on either size).""",
+                             styNormal))
+        lst.append(t3)
+        lst.append(PageBreak())
+
+        lst.append(Paragraph("""Row and Column spanning""", styleSheet['Heading1']))
+
+        lst.append(Paragraph("""This shows a very basic table.  We do a faint pink grid
+        to show what's behind it - imagine this is not printed, as we'll overlay it later
+        with some black lines.  We're going to "span" some cells, and have put a
+        value of None in the data to signify the cells we don't care about.
+        (In real life if you want an empty cell, put '' in it rather than None). """, styNormal))
+
+        sty = TableStyle([
+            #very faint grid to show what's where
+            ('GRID', (0,0), (-1,-1), 0.25, colors.pink),
+            ])
+
+        t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty)
+        lst.append(t)
+
+
+
+        lst.append(Paragraph("""We now center the text for the "period"
+        across the four cells for each quarter.  To do this we add a 'span'
+        command to the style to make the cell at row 1 column 3 cover 4 cells,
+        and a 'center' command for all cells in the top row. The spanning
+        is not immediately evident but trust us, it's happening - the word
+        'Period' is centered across the 4 columns.  Note also that the
+        underlying grid shows through.  All line drawing commands apply
+        to the underlying grid, so you have to take care what you put
+        grids through.""", styNormal))
+        sty = TableStyle([
+            #
+            ('GRID', (0,0), (-1,-1), 0.25, colors.pink),
+            ('ALIGN', (0,0), (-1,0), 'CENTER'),
+            ('SPAN', (2,0), (5,0)),
+            ])
+
+        t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty)
+        lst.append(t)
+
+        lst.append(Paragraph("""We repeat this for the words 'Region', Product'
+        and 'Total', which each span the top 2 rows; and for 'Nprth' and 'South'
+        which span 3 rows.  At the moment each cell's alignment is the default
+        (bottom), so these words appear to have "dropped down"; in fact they
+        are sitting on the bottom of their allocated ranges.  You will just see that
+        all the 'None' values vanished, as those cells are not drawn any more.""", styNormal))
+        sty = TableStyle([
+            #
+            ('GRID', (0,0), (-1,-1), 0.25, colors.pink),
+            ('ALIGN', (0,0), (-1,0), 'CENTER'),
+            ('SPAN', (2,0), (5,0)),
+            #span the other column heads down 2 rows
+            ('SPAN', (0,0), (0,1)),
+            ('SPAN', (1,0), (1,1)),
+            ('SPAN', (6,0), (6,1)),
+            #span the 'north' and 'south' down 3 rows each
+            ('SPAN', (0,2), (0,4)),
+            ('SPAN', (0,5), (0,7)),
+            ])
+
+        t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty)
+        lst.append(t)
+
+
+        lst.append(PageBreak())
+
+
+        lst.append(Paragraph("""Now we'll tart things up a bit.  First,
+        we set the vertical alignment of each spanned cell to 'middle'.
+        Next we add in some line drawing commands which do not slash across
+        the spanned cells (this needs a bit of work).
+        Finally we'll add some thicker lines to divide it up, and hide the pink.  Voila!
+        """, styNormal))
+        sty = TableStyle([
+            #
+#            ('GRID', (0,0), (-1,-1), 0.25, colors.pink),
+            ('TOPPADDING', (0,0), (-1,-1), 3),
+
+            #span the 'period'
+            ('SPAN', (2,0), (5,0)),
+            #span the other column heads down 2 rows
+            ('SPAN', (0,0), (0,1)),
+            ('SPAN', (1,0), (1,1)),
+            ('SPAN', (6,0), (6,1)),
+            #span the 'north' and 'south' down 3 rows each
+            ('SPAN', (0,2), (0,4)),
+            ('SPAN', (0,5), (0,7)),
+
+            #top row headings are centred
+            ('ALIGN', (0,0), (-1,0), 'CENTER'),
+            #everything we span is vertically centred
+            #span the other column heads down 2 rows
+            ('VALIGN', (0,0), (0,1), 'MIDDLE'),
+            ('VALIGN', (1,0), (1,1), 'MIDDLE'),
+            ('VALIGN', (6,0), (6,1), 'MIDDLE'),
+            #span the 'north' and 'south' down 3 rows each
+            ('VALIGN', (0,2), (0,4), 'MIDDLE'),
+            ('VALIGN', (0,5), (0,7), 'MIDDLE'),
+
+            #numeric stuff right aligned
+            ('ALIGN', (2,1), (-1,-1), 'RIGHT'),
+
+            #draw lines carefully so as not to swipe through
+            #any of the 'spanned' cells
+           ('GRID', (1,2), (-1,-1), 1.0, colors.black),
+            ('BOX', (0,2), (0,4), 1.0, colors.black),
+            ('BOX', (0,5), (0,7), 1.0, colors.black),
+            ('BOX', (0,0), (0,1), 1.0, colors.black),
+            ('BOX', (1,0), (1,1), 1.0, colors.black),
+
+            ('BOX', (2,0), (5,0), 1.0, colors.black),
+            ('GRID', (2,1), (5,1), 1.0, colors.black),
+
+            ('BOX', (6,0), (6,1), 1.0, colors.black),
+
+            # do fatter boxes around some cells            
+            ('BOX', (0,0), (-1,1), 2.0, colors.black),
+            ('BOX', (0,2), (-1,4), 2.0, colors.black),
+            ('BOX', (0,5), (-1,7), 2.0, colors.black),
+            ('BOX', (-1,0), (-1,-1), 2.0, colors.black),
+
+            ])
+
+        t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty)
+        lst.append(t)
+
+        lst.append(Paragraph("""How cells get sized""", styleSheet['Heading1']))
+
+        lst.append(Paragraph("""So far the table has been auto-sized.  This can be
+        computationally expensive, and can lead to yucky effects. Imagine a lot of
+        numbers, one of which goes to 4 figures - tha numeric column will be wider.
+        The best approach is to specify the column
+        widths where you know them, and let the system do the heights.  Here we set some
+        widths - an inch for the text columns and half an inch for the numeric ones.
+        """, styNormal))
+
+        t = Table(self.getDataBlock(),
+                    colWidths=(72,72,36,36,36,36,56),
+                    rowHeights=None,
+                    style=sty)
+        lst.append(t)
+
+        lst.append(Paragraph("""The auto-sized example 2 steps back demonstrates
+        one advanced feature of the sizing algorithm. In the table below,
+        the columns for Q1-Q4 should all be the same width.  We've made
+        the text above it a bit longer than "Period".  Note that this text
+        is technically in the 3rd column; on our first implementation this
+        was sized and column 3 was therefore quite wide.  To get it right,
+        we ensure that any cells which span columns, or which are 'overwritten'
+        by cells which span columns, are assigned zero width in the cell
+        sizing.  Thus, only the string 'Q1' and the numbers below it are
+        calculated in estimating the width of column 3, and the phrase
+        "What time of year?" is not used.  However, row-spanned cells are
+        taken into account. ALL the cells in the leftmost column
+        have a vertical span (or are occluded by others which do)
+        but it can still work out a sane width for them.
+        
+        """, styNormal))
+
+        data = self.getDataBlock()
+        data[0][2] = "Which time of year?"
+        #data[7][0] = Paragraph("Let's <b>really mess things up with a <i>paragraph</i>",styNormal)
+        t = Table(data,
+                    #colWidths=(72,72,36,36,36,36,56),
+                    rowHeights=None,
+                    style=sty)
+        lst.append(t)
+
+        lst.append(Paragraph("""Paragraphs and unsizeable objects in table cells.""", styleSheet['Heading1']))
+
+        lst.append(Paragraph("""Paragraphs and other flowable objects make table
+        sizing much harder. In general the height of a paragraph is a function
+        of its width so you can't ask it how wide it wants to be - and the
+        REALLY wide all-on-one-line solution is rarely what is wanted. We
+        refer to Paragraphs and their kin as "unsizeable objects". In this example
+        we have set the widths of all but the first column.  As you can see
+        it uses all the available space across the page for the first column.
+        Note also that this fairly large cell does NOT contribute to the
+        height calculation for its 'row'.  Under the hood it is in the
+        same row as the second Spam, but this row gets a height based on
+        its own contents and not the cell with the paragraph.
+        
+        """, styNormal))
+
+
+        data = self.getDataBlock()
+        data[5][0] = Paragraph("Let's <b>really mess things up</b> with a <i>paragraph</i>, whose height is a function of the width you give it.",styNormal)
+        t = Table(data,
+                    colWidths=(None,72,36,36,36,36,56),
+                    rowHeights=None,
+                    style=sty)
+        lst.append(t)
+
+
+        lst.append(Paragraph("""This one demonstrates that our current algorithm
+        does not cover all cases :-(  The height of row 0 is being driven by
+        the width of the para, which thinks it should fit in 1 column and not 4.
+        To really get this right would involve multiple passes through all the cells
+        applying rules until everything which can be sized is sized (possibly
+        backtracking), applying increasingly dumb and brutal
+        rules on each pass. 
+        """, styNormal))
+        data = self.getDataBlock()
+        data[0][2] = Paragraph("Let's <b>really mess things up</b> with a <i>paragraph</i>.",styNormal)
+        data[5][0] = Paragraph("Let's <b>really mess things up</b> with a <i>paragraph</i>, whose height is a function of the width you give it.",styNormal)
+        t = Table(data,
+                    colWidths=(None,72,36,36,36,36,56),
+                    rowHeights=None,
+                    style=sty)
+        lst.append(t)
+
+        lst.append(Paragraph("""To avoid these problems remember the golden rule
+        of ReportLab tables:  (1) fix the widths if you can, (2) don't use
+        a paragraph when a string will do.
+        """, styNormal))
+
+        SimpleDocTemplate('test_table_layout.pdf', showBoundary=1).build(lst)
+
+def makeSuite():
+    return makeSuiteForClasses(TableTestCase)
+
+
+#noruntests
+if __name__ == "__main__":
+    unittest.TextTestRunner().run(makeSuite())
+    print 'saved test_table_layout.pdf'
+
+    
\ No newline at end of file