tables.py: added in Gary Poster's gary@zope.com latest patch
authorrgbecker
Tue, 29 Mar 2005 13:54:51 +0000
changeset 2472 6795e616cdbe
parent 2471 76c745e075b2
child 2473 10d3b640b4f7
tables.py: added in Gary Poster's gary@zope.com latest patch
reportlab/platypus/tables.py
reportlab/test/test_table_layout.py
--- a/reportlab/platypus/tables.py	Wed Mar 23 13:35:58 2005 +0000
+++ b/reportlab/platypus/tables.py	Tue Mar 29 13:54:51 2005 +0000
@@ -190,6 +190,12 @@
             pass
     raise ValueError('Bad %s value %s in %s'%(name,value,str(cmd)))
 
+def _endswith(obj,s):
+    try:
+        return obj.endswith(s)
+    except:
+        return 0
+
 class Table(Flowable):
     def __init__(self, data, colWidths=None, rowHeights=None, style=None,
                 repeatRows=0, repeatCols=0, splitByRow=1, emptyTableAction=None, ident=None):
@@ -378,8 +384,7 @@
                 if type(w) in (FloatType,IntType): return w
             except AttributeError:
                 pass
-        if t is not StringType: v = v is not None and str(v) or ''
-        v = string.split(v, "\n")
+        v = string.split(v is not None and str(v) or '', "\n")
         return max(map(lambda a, b=s.fontname, c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v))
 
     def _calc_height(self, availHeight, availWidth, H=None, W=None):
@@ -435,9 +440,7 @@
                             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")
+                            v = string.split(v is not None and str(v) or '', "\n")
                             t = s.leading*len(v)
                         t = t+s.bottomPadding+s.topPadding
                     if t>h: h = t   #record a new maximum
@@ -532,7 +535,7 @@
             elif w == '*':
                 numberUndefined += 1
                 numberGreedyUndefined += 1
-            elif type(w) is StringType and w.endswith('%'):
+            elif _endswith(w,'%'):
                 percentDefined += 1
                 percentTotal += float(w[:-1])
             else:
@@ -550,7 +553,7 @@
         elementWidth = self._elementWidth
         for colNo in range(self._ncols):
             w = W[colNo]
-            if w is None or w=='*' or (type(w) is StringType and w.endswith('%')):
+            if w is None or w=='*' or _endswith(w,'%'):
                 siz = 1
                 current = final = None
                 for rowNo in range(self._nrows):
@@ -586,37 +589,73 @@
                 percentTotal = 100
                 defaultDesired = (defaultWeight/percentTotal)*availWidth
             else:
-                defaultWeight = defaultDesired = 0
-            
-            desiredWidths = {}
-            difference = 0
+                defaultWeight = defaultDesired = 1
+            # we now calculate how wide each column wanted to be, and then
+            # proportionately shrink that down to fit the remaining available
+            # space.  A column may not shrink less than its minimum width,
+            # however, which makes this a bit more complicated.
+            desiredWidths = []
+            totalDesired = 0
+            effectiveRemaining = remaining
             for colNo, minimum in minimums.items():
                 w = W[colNo]
-                if w is not None and w.endswith('%'):
+                if _endswith(w,'%'):
                     desired = (float(w[:-1])/percentTotal)*availWidth
                 elif w == '*':
                     desired = defaultDesired
                 else:
-                    desired = not numberGreedyUndefined and defaultDesired or 0
+                    desired = not numberGreedyUndefined and defaultDesired or 1
                 if desired <= minimum:
                     W[colNo] = minimum
                 else:
-                    desiredWidths[colNo] = desired
-                    difference += desired-minimum
-            disappointment = (difference-remaining)/len(desiredWidths)
-            for colNo, desired in desiredWidths.items():
-                adjusted = desired - disappointment
-                minimum = minimums[colNo]
-                if minimum > adjusted:
-                    W[colNo] = minimum
-                    del desiredWidths[colNo]
-                    difference += minimum-adjusted
-                    disappointment = (difference-remaining)/len(desiredWidths)
-            for colNo, desired in desiredWidths.items():
-                adjusted = desired - disappointment
-                minimum = minimums[colNo]
-                assert adjusted >= minimum
-                W[colNo] = adjusted
+                    desiredWidths.append(
+                        (desired-minimum, minimum, desired, colNo))
+                    totalDesired += desired
+                    effectiveRemaining += minimum
+            if desiredWidths: # else we're done
+                # let's say we have two variable columns.  One wanted
+                # 88 points, and one wanted 264 points.  The first has a 
+                # minWidth of 66, and the second of 55.  We have 71 points
+                # to divide up in addition to the totalMinimum (i.e., 
+                # remaining==71).  Our algorithm tries to keep the proportion
+                # of these variable columns.
+                #
+                # To do this, we add up the minimum widths of the variable
+                # columns and the remaining width.  That's 192.  We add up the
+                # totalDesired width.  That's 352.  That means we'll try to
+                # shrink the widths by a proportion of 192/352--.545454.
+                # That would make the first column 48 points, and the second
+                # 144 points--adding up to the desired 192.
+                #
+                # Unfortunately, that's too small for the first column.  It 
+                # must be 66 points.  Therefore, we go ahead and save that 
+                # column width as 88 points.  That leaves (192-88==) 104
+                # points remaining.  The proportion to shrink the remaining
+                # column is (104/264), which, multiplied  by the desired
+                # width of 264, is 104: the amount assigned to the remaining
+                # column.
+                proportion = effectiveRemaining/totalDesired
+                # we sort the desired widths by difference between desired and
+                # and minimum values, a value called "disappointment" in the 
+                # code.  This means that the columns with a bigger 
+                # disappointment will have a better chance of getting more of 
+                # the available space.
+                desiredWidths.sort()
+                finalSet = []
+                for disappointment, minimum, desired, colNo in desiredWidths:
+                    adjusted = proportion * desired
+                    if adjusted < minimum:
+                        W[colNo] = minimum
+                        totalDesired -= desired
+                        effectiveRemaining -= minimum
+                        if totalDesired:
+                            proportion = effectiveRemaining/totalDesired
+                    else:
+                        finalSet.append((minimum, desired, colNo))
+                for minimum, desired, colNo in finalSet:
+                    adjusted = proportion * desired
+                    assert adjusted >= minimum
+                    W[colNo] = adjusted
         else:
             for colNo, minimum in minimums.items():
                 W[colNo] = minimum
@@ -624,6 +663,24 @@
         self._argW = self._colWidths = W
         return W
 
+    def minWidth(self):
+        W = list(self._argW)
+        width = 0
+        elementWidth = self._elementWidth
+        for colNo, w in enumerate(W):
+            if w is None or w=='*' or _endswith(w,'%'):
+                final = 0
+                for rowNo in range(self._nrows):
+                    value = self._cellvalues[rowNo][colNo]
+                    style = self._cellStyles[rowNo][colNo]
+                    new = (elementWidth(value,style)+
+                           style.leftPadding+style.rightPadding)
+                    final = max(final, new)
+                width += final
+            else:
+                width += float(w)
+        return width # XXX + 1/2*(left and right border widths)
+
     def _calcSpanRanges(self):
         """Work out rects for tables which do row and column spanning.
 
@@ -1174,9 +1231,7 @@
                 x = colpos + colwidth - cellstyle.rightPadding
             else:
                 raise ValueError, 'Invalid justification %s' % just
-            if n is StringType: val = cellval
-            else: val = str(cellval)
-            vals = string.split(val, "\n")
+            vals = string.split(str(cellval), "\n")
             n = len(vals)
             leading = cellstyle.leading
             fontsize = cellstyle.fontsize
--- a/reportlab/test/test_table_layout.py	Wed Mar 23 13:35:58 2005 +0000
+++ b/reportlab/test/test_table_layout.py	Tue Mar 29 13:54:51 2005 +0000
@@ -338,6 +338,79 @@
         of ReportLab tables:  (1) fix the widths if you can, (2) don't use
         a paragraph when a string will do.
         """, styNormal))
+        
+        lst.append(Paragraph("""Unsized columns that contain flowables without
+        precise widths, such as paragraphs and nested tables,
+        still need to try and keep their content within borders and ideally 
+        even honor percentage requests.  This can be tricky--and expensive.  
+        But sometimes you can't follow the golden rules.
+        """, styNormal))
+
+        lst.append(Paragraph("""The code first calculates the minimum width
+        for each unsized column by iterating over every flowable in each column
+        and remembering the largest minimum width.  It then allocates
+        available space to accomodate the minimum widths.  Any remaining space
+        is divided up, treating a width of '*' as greedy, a width of None as
+        non-greedy, and a percentage as a weight.  If a column is already
+        wider than its percentage warrants, it is not further expanded, and
+        the other widths accomodate it.
+        """, styNormal))
+        
+        lst.append(Paragraph("""For instance, consider this tortured table.
+        It contains four columns, with widths of None, None, 60%, and 20%,
+        respectively, and a single row.  The first cell contains a paragraph.
+        The second cell contains a table with fixed column widths that total 
+        about 50% of the total available table width.  The third cell contains
+        a string.  The last cell contains a table with no set widths but a 
+        single cell containing a paragraph.
+        """, styNormal))
+        ministy = TableStyle([
+            ('GRID', (0,0), (-1,-1), 1.0, colors.black),
+            ])
+        nested1 = [Paragraph(
+            'This is a paragraph.  The column has a width of None.', 
+            styNormal)]
+        nested2 = [Table(
+            [[Paragraph(
+                'This table is set to take up two and a half inches.  The '
+                'column that holds it has a width of None.', styNormal)]],
+            colWidths=(180,),
+            rowHeights=None,
+            style=ministy)]
+        nested3 = '60% width'
+        nested4 = [Table(
+            [[[Paragraph(
+                "This is a table with a paragraph in it but no width set.  "
+                "The column width in the containing table is 20%.", 
+                styNormal)]]],
+            colWidths=(None,),
+            rowHeights=None,
+            style=ministy)]
+        t = Table([[nested1, nested2, nested3, nested4]],
+                  colWidths=(None, None, '60%', '20%'),
+                  rowHeights=None,
+                  style=ministy)
+        lst.append(t)
+        
+        lst.append(Paragraph("""Notice that the second column does expand to
+        account for the minimum size of its contents; and that the remaining
+        space goes to the third column, in an attempt to honor the '60%' 
+        request as much as possible.  This is reminiscent of the typical HTML
+        browser approach to tables.""", styNormal))
+
+        lst.append(Paragraph("""To get an idea of how potentially expensive
+        this is, consider the case of the last column: the table gets the
+        minimum width of every flowable of every cell in the column.  In this
+        case one of the flowables is a table with a column without a set
+        width, so the nested table must itself iterate over its flowables. 
+        The contained paragraph then calculates the width of every word in it
+        to see what the biggest word is, given the set font face and size.  It
+        is easy to imagine creating a structure of this sort that took an
+        unacceptably large amount of time to calculate.  Remember the golden
+        rule, if you can. """, styNormal))
+        
+        lst.append(Paragraph("""This code does not yet handle spans well.""",
+        styNormal))
 
         SimpleDocTemplate(outputfile('test_table_layout.pdf'), showBoundary=1).build(lst)