experimental support for pie shading; version-->3.5.37
authorrobin
Fri, 07 Feb 2020 13:31:38 +0000
changeset 4572 126dc1fe9e68
parent 4571 379a3156cb67
child 4573 82a14db33e3e
experimental support for pie shading; version-->3.5.37
CHANGES.md
src/reportlab/__init__.py
src/reportlab/graphics/charts/doughnut.py
src/reportlab/graphics/charts/piecharts.py
src/reportlab/lib/validators.py
--- a/CHANGES.md	Tue Jan 28 14:18:45 2020 +0000
+++ b/CHANGES.md	Fri Feb 07 13:31:38 2020 +0000
@@ -11,6 +11,10 @@
 The contributors lists are in no order and apologies to those accidentally not
 mentioned. If we missed you, please let us know!
 
+RELEASE 3.5.37	07/02/2020
+--------------------------
+	* experimental support for 2d pie/doughnut shading
+
 RELEASE 3.5.36	28/01/2020
 --------------------------
 	* update travis version of multibuild contrib by Matthew Brett
--- a/src/reportlab/__init__.py	Tue Jan 28 14:18:45 2020 +0000
+++ b/src/reportlab/__init__.py	Fri Feb 07 13:31:38 2020 +0000
@@ -1,9 +1,9 @@
 #Copyright ReportLab Europe Ltd. 2000-2018
 #see license.txt for license details
 __doc__="""The Reportlab PDF generation library."""
-Version = "3.5.36"
+Version = "3.5.37"
 __version__=Version
-__date__='20200128'
+__date__='20200207'
 
 import sys, os
 
--- a/src/reportlab/graphics/charts/doughnut.py	Tue Jan 28 14:18:45 2020 +0000
+++ b/src/reportlab/graphics/charts/doughnut.py	Fri Feb 07 13:31:38 2020 +0000
@@ -20,7 +20,8 @@
                                     isBoolean, isListOfColors,\
                                     isNoneOrListOfNoneOrStrings,\
                                     isNoneOrListOfNoneOrNumbers,\
-                                    isNumberOrNone
+                                    isNumberOrNone, isListOfNoneOrNumber,\
+                                    isListOfListOfNoneOrNumber, EitherOr
 from reportlab.lib.attrmap import *
 from reportlab.pdfgen.canvas import Canvas
 from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \
@@ -30,6 +31,7 @@
 from reportlab.graphics.charts.textlabels import Label
 from reportlab.graphics.widgets.markers import Marker
 from functools import reduce
+from reportlab import xrange
 
 class SectorProperties(WedgeProperties):
     """This holds descriptive information about the sectors in a doughnut chart.
@@ -48,7 +50,7 @@
         y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'),
         width = AttrMapValue(isNumber, desc='width of doughnut bounding box. Need not be same as width.'),
         height = AttrMapValue(isNumber, desc='height of doughnut bounding box.  Need not be same as height.'),
-        data = AttrMapValue(None, desc='list of numbers defining sector sizes; need not sum to 1'),
+        data = AttrMapValue(EitherOr((isListOfNoneOrNumber,isListOfListOfNoneOrNumber)), desc='list of numbers defining sector sizes; need not sum to 1'),
         labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
         startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
         direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
@@ -120,17 +122,19 @@
 
     def makeSectors(self):
         # normalize slice data
-        if isinstance(self.data,(list,tuple)) and isinstance(self.data[0],(list,tuple)):
+        data = self.data
+        multi = isListOfListOfNoneOrNumber(data)
+        if multi:
             #it's a nested list, more than one sequence
             normData = []
             n = []
-            for l in self.data:
+            for l in data:
                 t = self.normalizeData(l)
                 normData.append(t)
                 n.append(len(t))
             self._seriesCount = max(n)
         else:
-            normData = self.normalizeData(self.data)
+            normData = self.normalizeData(data)
             n = len(normData)
             self._seriesCount = n
         
@@ -139,18 +143,18 @@
         L = []
         L_add = L.append
         
-        if self.labels is None:
+        labels = self.labels
+        if labels is None:
             labels = []
-            if not isinstance(n,(list,tuple)):
+            if not multi:
                 labels = [''] * n
             else:
                 for m in n:
                     labels = list(labels) + [''] * m
         else:
-            labels = self.labels
             #there's no point in raising errors for less than enough labels if
             #we silently create all for the extreme case of no labels.
-            if not isinstance(n,(list,tuple)):
+            if not multi:
                 i = n-len(labels)
                 if i>0:
                     labels = list(labels) + [''] * i
@@ -161,6 +165,7 @@
                 i = tlab-len(labels)
                 if i>0:
                     labels = list(labels) + [''] * i
+        self.labels = labels
 
         xradius = self.width/2.0
         yradius = self.height/2.0
@@ -177,9 +182,10 @@
         startAngle = self.startAngle #% 360
         styleCount = len(self.slices)
         irf = self.innerRadiusFraction
-        if isinstance(self.data[0],(list,tuple)):
+
+        if multi:
             #multi-series doughnut
-            ndata = len(self.data)
+            ndata = len(data)
             if irf is None:
                 yir = (yradius/2.5)/ndata
                 xir = (xradius/2.5)/ndata
@@ -191,7 +197,8 @@
             for sn,series in enumerate(normData):
                 for i,angle in enumerate(series):
                     endAngle = (startAngle + (angle * whichWay)) #% 360
-                    if abs(startAngle-endAngle)<1e-5:
+                    aa = abs(startAngle-endAngle)
+                    if aa<1e-5:
                         startAngle = endAngle
                         continue
                     if startAngle < endAngle:
@@ -204,7 +211,7 @@
 
                     #if we didn't use %stylecount here we'd end up with the later sectors
                     #all having the default style
-                    sectorStyle = self.slices[i%styleCount]
+                    sectorStyle = self.slices[sn,i%styleCount]
 
                     # is it a popout?
                     cx, cy = centerx, centery
@@ -220,7 +227,7 @@
                     yr = yr1 + ydr
                     xr1 = xir+sn*xdr
                     xr = xr1 + xdr
-                    if isinstance(n,(list,tuple)):
+                    if len(series) > 1:
                         theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1)
                     else:
                         theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
@@ -230,6 +237,35 @@
                     theSector.strokeWidth = sectorStyle.strokeWidth
                     theSector.strokeDashArray = sectorStyle.strokeDashArray
 
+                    shader = sectorStyle.shadingKind
+                    if shader:
+                        nshades = aa / float(sectorStyle.shadingAngle)
+                        if nshades > 1:
+                            shader = colors.Whiter if shader=='lighten' else colors.Blacker
+                            nshades = 1+int(nshades)
+                            shadingAmount = 1-sectorStyle.shadingAmount
+                            if sectorStyle.shadingDirection=='normal':
+                                dsh = (1-shadingAmount)/float(nshades-1)
+                                shf1 = shadingAmount
+                            else:
+                                dsh = (shadingAmount-1)/float(nshades-1)
+                                shf1 = 1
+                            shda = (a2-a1)/float(nshades)
+                            shsc = sectorStyle.fillColor
+                            theSector.fillColor = None
+                            for ish in xrange(nshades):
+                                sha1 = a1 + ish*shda
+                                sha2 = a1 + (ish+1)*shda
+                                shc = shader(shsc,shf1 + dsh*ish)
+                                if len(series)>1:
+                                    shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1)
+                                else:
+                                    shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
+                                shSector.fillColor = shc
+                                shSector.strokeColor = None
+                                shSector.strokeWidth = 0
+                                g.add(shSector)
+
                     g.add(theSector)
 
                     if sn == 0 and sectorStyle.visible and sectorStyle.label_visible:
@@ -260,7 +296,8 @@
                 xir = xradius*irf
             for i,angle in enumerate(normData):
                 endAngle = (startAngle + (angle * whichWay)) #% 360
-                if abs(startAngle-endAngle)<1e-5:
+                aa = abs(startAngle-endAngle)
+                if aa<1e-5:
                     startAngle = endAngle
                     continue
                 if startAngle < endAngle:
@@ -295,6 +332,35 @@
                 theSector.strokeWidth = sectorStyle.strokeWidth
                 theSector.strokeDashArray = sectorStyle.strokeDashArray
 
+                shader = sectorStyle.shadingKind
+                if shader:
+                    nshades = aa / float(sectorStyle.shadingAngle)
+                    if nshades > 1:
+                        shader = colors.Whiter if shader=='lighten' else colors.Blacker
+                        nshades = 1+int(nshades)
+                        shadingAmount = 1-sectorStyle.shadingAmount
+                        if sectorStyle.shadingDirection=='normal':
+                            dsh = (1-shadingAmount)/float(nshades-1)
+                            shf1 = shadingAmount
+                        else:
+                            dsh = (shadingAmount-1)/float(nshades-1)
+                            shf1 = 1
+                        shda = (a2-a1)/float(nshades)
+                        shsc = sectorStyle.fillColor
+                        theSector.fillColor = None
+                        for ish in xrange(nshades):
+                            sha1 = a1 + ish*shda
+                            sha2 = a1 + (ish+1)*shda
+                            shc = shader(shsc,shf1 + dsh*ish)
+                            if n > 1:
+                                shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir)
+                            elif n==1:
+                                shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
+                            shSector.fillColor = shc
+                            shSector.strokeColor = None
+                            shSector.strokeWidth = 0
+                            g.add(shSector)
+
                 g.add(theSector)
 
                 # now draw a label
--- a/src/reportlab/graphics/charts/piecharts.py	Tue Jan 28 14:18:45 2020 +0000
+++ b/src/reportlab/graphics/charts/piecharts.py	Fri Feb 07 13:31:38 2020 +0000
@@ -105,6 +105,10 @@
         label_pointer_piePad = AttrMapValue(isNumber,desc='pad between pointer label and pie'),
         swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ...",advancedUsage=1),
         visible = AttrMapValue(isBoolean,'Set to false to skip displaying'),
+        shadingAmount = AttrMapValue(isNumberOrNone,desc='amount by which to shade fillColor'),
+        shadingAngle = AttrMapValue(isNumber,desc='shading changes at multiple of this angle (in degrees)'),
+        shadingDirection = AttrMapValue(OneOf('normal','anti'),desc="Whether shading is at start or end of wedge/sector"),
+        shadingKind = AttrMapValue(OneOf(None,'lighten','darken'),desc="use colors.Whiter or Blacker"),
         )
 
     def __init__(self):
@@ -139,6 +143,10 @@
         self.label_pointer_edgePad = 2
         self.label_pointer_piePad = 3
         self.visible = 1
+        self.shadingKind = None
+        self.shadingAmount = 0.5
+        self.shadingAngle = 2.0137
+        self.shadingDirection = 'normal'    #or 'anti'
 
 def _addWedgeLabel(self,text,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel):
     # now draw a label
@@ -764,6 +772,7 @@
 
         innerRadiusFraction = self.innerRadiusFraction
 
+
         for i,(a1,a2) in angles:
             if a2 is None: continue
             #if we didn't use %stylecount here we'd end up with the later wedges
@@ -802,9 +811,38 @@
             theWedge.strokeLineJoin = wedgeStyle.strokeLineJoin
             theWedge.strokeLineCap = wedgeStyle.strokeLineCap
             theWedge.strokeMiterLimit = wedgeStyle.strokeMiterLimit
-            theWedge.strokeWidth = wedgeStyle.strokeWidth
             theWedge.strokeDashArray = wedgeStyle.strokeDashArray
 
+            shader = wedgeStyle.shadingKind
+            if shader:
+                nshades = aa / float(wedgeStyle.shadingAngle)
+                if nshades > 1:
+                    shader = colors.Whiter if shader=='lighten' else colors.Blacker
+                    nshades = 1+int(nshades)
+                    shadingAmount = 1-wedgeStyle.shadingAmount
+                    if wedgeStyle.shadingDirection=='normal':
+                        dsh = (1-shadingAmount)/float(nshades-1)
+                        shf1 = shadingAmount
+                    else:
+                        dsh = (shadingAmount-1)/float(nshades-1)
+                        shf1 = 1
+                    shda = (a2-a1)/float(nshades)
+                    shsc = wedgeStyle.fillColor
+                    theWedge.fillColor = None
+                    for ish in xrange(nshades):
+                        sha1 = a1 + ish*shda
+                        sha2 = a1 + (ish+1)*shda
+                        shc = shader(shsc,shf1 + dsh*ish)
+                        if innerRadiusFraction:
+                            shWedge = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius,
+                                    radius1=xradius*innerRadiusFraction,yradius1=yradius*innerRadiusFraction)
+                        else:
+                            shWedge = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius)
+                        shWedge.fillColor = shc
+                        shWedge.strokeColor = None
+                        shWedge.strokeWidth = 0
+                        g_add(shWedge)
+
             g_add(theWedge)
             if wr:
                 wr(theWedge,value=a1._data,label=text)
--- a/src/reportlab/lib/validators.py	Tue Jan 28 14:18:45 2020 +0000
+++ b/src/reportlab/lib/validators.py	Fri Feb 07 13:31:38 2020 +0000
@@ -346,6 +346,8 @@
 isNumberOrNone = _isNumberOrNone()
 isTextAnchor = OneOf('start','middle','end','boxauto')
 isListOfNumbers = SequenceOf(isNumber,'isListOfNumbers')
+isListOfNoneOrNumber = SequenceOf(isNumberOrNone,'isListOfNoneOrNumber')
+isListOfListOfNoneOrNumber = SequenceOf(isListOfNoneOrNumber,'isListOfListOfNoneOrNumber')
 isListOfNumbersOrNone = _isListOfNumbersOrNone()
 isListOfShapes = _isListOfShapes()
 isListOfStrings = SequenceOf(isString,'isListOfStrings')