Added printing requirements

This commit is contained in:
Tom Price
2014-12-07 17:32:24 +00:00
parent c76d12d877
commit 7ba11a2db9
571 changed files with 143368 additions and 6 deletions

View File

@@ -0,0 +1,5 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/__init__.py
__version__=''' $Id$ '''
__doc__='''Business charts'''

View File

@@ -0,0 +1,94 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/areas.py
__version__=''' $Id$ '''
__doc__='''This module defines a Area mixin classes'''
from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isNoneOrShape
from reportlab.graphics.widgetbase import Widget
from reportlab.graphics.shapes import Rect, Group, Line, Polygon
from reportlab.lib.attrmap import AttrMap, AttrMapValue
from reportlab.lib.colors import grey
class PlotArea(Widget):
"Abstract base class representing a chart's plot area, pretty unusable by itself."
_attrMap = AttrMap(
x = AttrMapValue(isNumber, desc='X position of the lower-left corner of the chart.'),
y = AttrMapValue(isNumber, desc='Y position of the lower-left corner of the chart.'),
width = AttrMapValue(isNumber, desc='Width of the chart.'),
height = AttrMapValue(isNumber, desc='Height of the chart.'),
strokeColor = AttrMapValue(isColorOrNone, desc='Color of the plot area border.'),
strokeWidth = AttrMapValue(isNumber, desc='Width plot area border.'),
fillColor = AttrMapValue(isColorOrNone, desc='Color of the plot area interior.'),
background = AttrMapValue(isNoneOrShape, desc='Handle to background object e.g. Rect(0,0,width,height).'),
debug = AttrMapValue(isNumber, desc='Used only for debugging.'),
)
def __init__(self):
self.x = 20
self.y = 10
self.height = 85
self.width = 180
self.strokeColor = None
self.strokeWidth = 1
self.fillColor = None
self.background = None
self.debug = 0
def makeBackground(self):
if self.background is not None:
BG = self.background
if isinstance(BG,Group):
g = BG
for bg in g.contents:
bg.x = self.x
bg.y = self.y
bg.width = self.width
bg.height = self.height
else:
g = Group()
if type(BG) not in (type(()),type([])): BG=(BG,)
for bg in BG:
bg.x = self.x
bg.y = self.y
bg.width = self.width
bg.height = self.height
g.add(bg)
return g
else:
strokeColor,strokeWidth,fillColor=self.strokeColor, self.strokeWidth, self.fillColor
if (strokeWidth and strokeColor) or fillColor:
g = Group()
_3d_dy = getattr(self,'_3d_dy',None)
x = self.x
y = self.y
h = self.height
w = self.width
if _3d_dy is not None:
_3d_dx = self._3d_dx
if fillColor and not strokeColor:
from reportlab.lib.colors import Blacker
c = Blacker(fillColor, getattr(self,'_3d_blacken',0.7))
else:
c = strokeColor
if not strokeWidth: strokeWidth = 0.5
if fillColor or strokeColor or c:
bg = Polygon([x,y,x,y+h,x+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fillColor)
g.add(bg)
g.add(Line(x,y,x+_3d_dx,y+_3d_dy, strokeWidth=0.5, strokeColor=c))
g.add(Line(x+_3d_dx,y+_3d_dy, x+_3d_dx,y+h+_3d_dy,strokeWidth=0.5, strokeColor=c))
fc = Blacker(c, getattr(self,'_3d_blacken',0.8))
g.add(Polygon([x,y,x+_3d_dx,y+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fc))
bg = Line(x+_3d_dx,y+_3d_dy, x+w+_3d_dx,y+_3d_dy,strokeWidth=0.5, strokeColor=c)
else:
bg = None
else:
bg = Rect(x, y, w, h,
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor)
if bg: g.add(bg)
return g
else:
return None

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
from reportlab.lib.colors import blue, _PCMYK_black
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String
from reportlab.graphics.widgetbase import Widget
from reportlab.lib.attrmap import *
from reportlab.lib.validators import *
from reportlab.lib.units import cm
from reportlab.pdfbase.pdfmetrics import getFont
from reportlab.graphics.charts.lineplots import _maxWidth
class DotBox(Widget):
"""Returns a dotbox widget."""
#Doesn't use TypedPropertyCollection for labels - this can be a later improvement
_attrMap = AttrMap(
xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
desc="List of text labels for boxes on left hand side"),
ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
desc="Text label for second box on left hand side"),
labelFontName = AttrMapValue(isString,
desc="Name of font used for the labels"),
labelFontSize = AttrMapValue(isNumber,
desc="Size of font used for the labels"),
labelOffset = AttrMapValue(isNumber,
desc="Space between label text and grid edge"),
strokeWidth = AttrMapValue(isNumber,
desc='Width of the grid and dot outline'),
gridDivWidth = AttrMapValue(isNumber,
desc="Width of each 'box'"),
gridColor = AttrMapValue(isColor,
desc='Colour for the box and gridding'),
dotDiameter = AttrMapValue(isNumber,
desc="Diameter of the circle used for the 'dot'"),
dotColor = AttrMapValue(isColor,
desc='Colour of the circle on the box'),
dotXPosition = AttrMapValue(isNumber,
desc='X Position of the circle'),
dotYPosition = AttrMapValue(isNumber,
desc='X Position of the circle'),
x = AttrMapValue(isNumber,
desc='X Position of dotbox'),
y = AttrMapValue(isNumber,
desc='Y Position of dotbox'),
)
def __init__(self):
self.xlabels=["Value", "Blend", "Growth"]
self.ylabels=["Small", "Medium", "Large"]
self.labelFontName = "Helvetica"
self.labelFontSize = 6
self.labelOffset = 5
self.strokeWidth = 0.5
self.gridDivWidth=0.5*cm
self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0)
self.dotDiameter=0.4*cm
self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0)
self.dotXPosition = 1
self.dotYPosition = 1
self.x = 30
self.y = 5
def _getDrawingDimensions(self):
leftPadding=rightPadding=topPadding=bottomPadding=5
#find width of grid
tx=len(self.xlabels)*self.gridDivWidth
#add padding (and offset)
tx=tx+leftPadding+rightPadding+self.labelOffset
#add in maximum width of text
tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize)
#find height of grid
ty=len(self.ylabels)*self.gridDivWidth
#add padding (and offset)
ty=ty+topPadding+bottomPadding+self.labelOffset
#add in maximum width of text
ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize)
#print (tx, ty)
return (tx,ty)
def demo(self,drawing=None):
if not drawing:
tx,ty=self._getDrawingDimensions()
drawing = Drawing(tx,ty)
drawing.add(self.draw())
return drawing
def draw(self):
g = Group()
#box
g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth,
strokeColor=self.gridColor,
strokeWidth=self.strokeWidth,
fillColor=None))
#internal gridding
for f in range (1,len(self.ylabels)):
#horizontal
g.add(Line(strokeColor=self.gridColor,
strokeWidth=self.strokeWidth,
x1 = self.x,
y1 = self.y+f*self.gridDivWidth,
x2 = self.x+len(self.xlabels)*self.gridDivWidth,
y2 = self.y+f*self.gridDivWidth))
for f in range (1,len(self.xlabels)):
#vertical
g.add(Line(strokeColor=self.gridColor,
strokeWidth=self.strokeWidth,
x1 = self.x+f*self.gridDivWidth,
y1 = self.y,
x2 = self.x+f*self.gridDivWidth,
y2 = self.y+len(self.ylabels)*self.gridDivWidth))
# draw the 'dot'
g.add(Circle(strokeColor=self.gridColor,
strokeWidth=self.strokeWidth,
fillColor=self.dotColor,
cx = self.x+(self.dotXPosition*self.gridDivWidth),
cy = self.y+(self.dotYPosition*self.gridDivWidth),
r = self.dotDiameter/2.0))
#used for centering y-labels (below)
ascent=getFont(self.labelFontName).face.ascent
if ascent==0:
ascent=0.718 # default (from helvetica)
ascent=ascent*self.labelFontSize # normalize
#do y-labels
if self.ylabels != None:
for f in range (len(self.ylabels)-1,-1,-1):
if self.ylabels[f]!= None:
g.add(String(strokeColor=self.gridColor,
text = self.ylabels[f],
fontName = self.labelFontName,
fontSize = self.labelFontSize,
fillColor=_PCMYK_black,
x = self.x-self.labelOffset,
y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0),
textAnchor = 'end'))
#do x-labels
if self.xlabels != None:
for f in range (0,len(self.xlabels)):
if self.xlabels[f]!= None:
l=Label()
l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0
l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset
l.angle=90
l.textAnchor='start'
l.fontName = self.labelFontName
l.fontSize = self.labelFontSize
l.fillColor = _PCMYK_black
l.setText(self.xlabels[f])
l.boxAnchor = 'sw'
l.draw()
g.add(l)
return g
if __name__ == "__main__":
d = DotBox()
d.demo().save(fnRoot="dotbox")

View File

@@ -0,0 +1,400 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/doughnut.py
# doughnut chart
__version__=''' $Id$ '''
__doc__="""Doughnut chart
Produces a circular chart like the doughnut charts produced by Excel.
Can handle multiple series (which produce concentric 'rings' in the chart).
"""
import copy
from math import sin, cos, pi
from reportlab.lib import colors
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
isListOfNumbers, isColorOrNone, isString,\
isListOfStringsOrNone, OneOf, SequenceOf,\
isBoolean, isListOfColors,\
isNoneOrListOfNoneOrStrings,\
isNoneOrListOfNoneOrNumbers,\
isNumberOrNone
from reportlab.lib.attrmap import *
from reportlab.pdfgen.canvas import Canvas
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \
Wedge, String, SolidShape, UserNode, STATE_DEFAULTS
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel, fixLabelOverlaps
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.widgets.markers import Marker
from functools import reduce
class SectorProperties(WedgeProperties):
"""This holds descriptive information about the sectors in a doughnut chart.
It is not to be confused with the 'sector itself'; this just holds
a recipe for how to format one, and does not allow you to hack the
angles. It can format a genuine Sector object for you with its
format method.
"""
_attrMap = AttrMap(BASE=WedgeProperties,
)
class Doughnut(AbstractPieChart):
_attrMap = AttrMap(
x = AttrMapValue(isNumber, desc='X position of the chart within its container.'),
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'),
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'"),
slices = AttrMapValue(None, desc="collection of sector descriptor objects"),
simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"),
# advanced usage
checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix\n standard label overlaps(default off)",advancedUsage=1),
sideLabels = AttrMapValue(isBoolean, desc="If true attempt to make chart with labels along side and pointers", advancedUsage=1)
)
def __init__(self):
self.x = 0
self.y = 0
self.width = 100
self.height = 100
self.data = [1,1]
self.labels = None # or list of strings
self.startAngle = 90
self.direction = "clockwise"
self.simpleLabels = 1
self.checkLabelOverlap = 0
self.sideLabels = 0
self.slices = TypedPropertyCollection(SectorProperties)
self.slices[0].fillColor = colors.darkcyan
self.slices[1].fillColor = colors.blueviolet
self.slices[2].fillColor = colors.blue
self.slices[3].fillColor = colors.cyan
self.slices[4].fillColor = colors.pink
self.slices[5].fillColor = colors.magenta
self.slices[6].fillColor = colors.yellow
def demo(self):
d = Drawing(200, 100)
dn = Doughnut()
dn.x = 50
dn.y = 10
dn.width = 100
dn.height = 80
dn.data = [10,20,30,40,50,60]
dn.labels = ['a','b','c','d','e','f']
dn.slices.strokeWidth=0.5
dn.slices[3].popout = 10
dn.slices[3].strokeWidth = 2
dn.slices[3].strokeDashArray = [2,2]
dn.slices[3].labelRadius = 1.75
dn.slices[3].fontColor = colors.red
dn.slices[0].fillColor = colors.darkcyan
dn.slices[1].fillColor = colors.blueviolet
dn.slices[2].fillColor = colors.blue
dn.slices[3].fillColor = colors.cyan
dn.slices[4].fillColor = colors.aquamarine
dn.slices[5].fillColor = colors.cadetblue
dn.slices[6].fillColor = colors.lightcoral
d.add(dn)
return d
def normalizeData(self, data=None):
from operator import add
sum = float(reduce(add,data,0))
return abs(sum)>=1e-8 and list(map(lambda x,f=360./sum: f*x, data)) or len(data)*[0]
def makeSectors(self):
# normalize slice data
if isinstance(self.data,(list,tuple)) and isinstance(self.data[0],(list,tuple)):
#it's a nested list, more than one sequence
normData = []
n = []
for l in self.data:
t = self.normalizeData(l)
normData.append(t)
n.append(len(t))
self._seriesCount = max(n)
else:
normData = self.normalizeData(self.data)
n = len(normData)
self._seriesCount = n
#labels
checkLabelOverlap = self.checkLabelOverlap
L = []
L_add = L.append
if self.labels is None:
labels = []
if not isinstance(n,(list,tuple)):
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)):
i = n-len(labels)
if i>0:
labels = list(labels) + [''] * i
else:
tlab = 0
for m in n:
tlab += m
i = tlab-len(labels)
if i>0:
labels = list(labels) + [''] * i
xradius = self.width/2.0
yradius = self.height/2.0
centerx = self.x + xradius
centery = self.y + yradius
if self.direction == "anticlockwise":
whichWay = 1
else:
whichWay = -1
g = Group()
startAngle = self.startAngle #% 360
styleCount = len(self.slices)
if isinstance(self.data[0],(list,tuple)):
#multi-series doughnut
ndata = len(self.data)
yir = (yradius/2.5)/ndata
xir = (xradius/2.5)/ndata
ydr = (yradius-yir)/ndata
xdr = (xradius-xir)/ndata
for sn,series in enumerate(normData):
for i,angle in enumerate(series):
endAngle = (startAngle + (angle * whichWay)) #% 360
if abs(startAngle-endAngle)<1e-5:
startAngle = endAngle
continue
if startAngle < endAngle:
a1 = startAngle
a2 = endAngle
else:
a1 = endAngle
a2 = startAngle
startAngle = endAngle
#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]
# is it a popout?
cx, cy = centerx, centery
if sectorStyle.popout != 0:
# pop out the sector
averageAngle = (a1+a2)/2.0
aveAngleRadians = averageAngle * pi/180.0
popdistance = sectorStyle.popout
cx = centerx + popdistance * cos(aveAngleRadians)
cy = centery + popdistance * sin(aveAngleRadians)
yr1 = yir+sn*ydr
yr = yr1 + ydr
xr1 = xir+sn*xdr
xr = xr1 + xdr
if isinstance(n,(list,tuple)):
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)
theSector.fillColor = sectorStyle.fillColor
theSector.strokeColor = sectorStyle.strokeColor
theSector.strokeWidth = sectorStyle.strokeWidth
theSector.strokeDashArray = sectorStyle.strokeDashArray
g.add(theSector)
if sn == 0:
text = self.getSeriesName(i,'')
if text:
averageAngle = (a1+a2)/2.0
aveAngleRadians = averageAngle*pi/180.0
labelRadius = sectorStyle.labelRadius
rx = xradius*labelRadius
ry = yradius*labelRadius
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
l = _addWedgeLabel(self,text,averageAngle,labelX,labelY,sectorStyle)
if checkLabelOverlap:
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
'bounds': l.getBounds(),
}
L_add(l)
else:
#single series doughnut
yir = yradius/2.5
xir = xradius/2.5
for i,angle in enumerate(normData):
endAngle = (startAngle + (angle * whichWay)) #% 360
if abs(startAngle-endAngle)<1e-5:
startAngle = endAngle
continue
if startAngle < endAngle:
a1 = startAngle
a2 = endAngle
else:
a1 = endAngle
a2 = startAngle
startAngle = endAngle
#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]
# is it a popout?
cx, cy = centerx, centery
if sectorStyle.popout != 0:
# pop out the sector
averageAngle = (a1+a2)/2.0
aveAngleRadians = averageAngle * pi/180.0
popdistance = sectorStyle.popout
cx = centerx + popdistance * cos(aveAngleRadians)
cy = centery + popdistance * sin(aveAngleRadians)
if n > 1:
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir)
elif n==1:
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
theSector.fillColor = sectorStyle.fillColor
theSector.strokeColor = sectorStyle.strokeColor
theSector.strokeWidth = sectorStyle.strokeWidth
theSector.strokeDashArray = sectorStyle.strokeDashArray
g.add(theSector)
# now draw a label
if labels[i] != "":
averageAngle = (a1+a2)/2.0
aveAngleRadians = averageAngle*pi/180.0
labelRadius = sectorStyle.labelRadius
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
rx = xradius*labelRadius
ry = yradius*labelRadius
l = _addWedgeLabel(self,labels[i],averageAngle,labelX,labelY,sectorStyle)
if checkLabelOverlap:
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
'bounds': l.getBounds(),
}
L_add(l)
if checkLabelOverlap and L:
fixLabelOverlaps(L)
for l in L: g.add(l)
return g
def draw(self):
g = Group()
g.add(self.makeSectors())
return g
def sample1():
"Make up something from the individual Sectors"
d = Drawing(400, 400)
g = Group()
s1 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=0, endangledegrees=120, radius1=100)
s1.fillColor=colors.red
s1.strokeColor=None
d.add(s1)
s2 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=120, endangledegrees=240, radius1=100)
s2.fillColor=colors.green
s2.strokeColor=None
d.add(s2)
s3 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=240, endangledegrees=260, radius1=100)
s3.fillColor=colors.blue
s3.strokeColor=None
d.add(s3)
s4 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=260, endangledegrees=360, radius1=100)
s4.fillColor=colors.gray
s4.strokeColor=None
d.add(s4)
return d
def sample2():
"Make a simple demo"
d = Drawing(400, 400)
dn = Doughnut()
dn.x = 50
dn.y = 50
dn.width = 300
dn.height = 300
dn.data = [10,20,30,40,50,60]
d.add(dn)
return d
def sample3():
"Make a more complex demo"
d = Drawing(400, 400)
dn = Doughnut()
dn.x = 50
dn.y = 50
dn.width = 300
dn.height = 300
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
dn.labels = ['a','b','c','d','e','f']
d.add(dn)
return d
def sample4():
"Make a more complex demo with Label Overlap fixing"
d = Drawing(400, 400)
dn = Doughnut()
dn.x = 50
dn.y = 50
dn.width = 300
dn.height = 300
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
dn.labels = ['a','b','c','d','e','f']
dn.checkLabelOverlap = True
d.add(dn)
return d
if __name__=='__main__':
from reportlab.graphics.renderPDF import drawToFile
d = sample1()
drawToFile(d, 'doughnut1.pdf')
d = sample2()
drawToFile(d, 'doughnut2.pdf')
d = sample3()
drawToFile(d, 'doughnut3.pdf')

View File

@@ -0,0 +1,716 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/legends.py
__version__=''' $Id$ '''
__doc__="""This will be a collection of legends to be used with charts."""
import copy, operator
from reportlab.lib import colors
from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\
isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\
EitherOr, NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf, isInstanceOf
from reportlab.lib.attrmap import *
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS
from reportlab.graphics.charts.areas import PlotArea
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol
from reportlab.lib.utils import isSeq, find_locals
from reportlab.graphics.shapes import _baseGFontName
from functools import reduce
def _transMax(n,A):
X = n*[0]
m = 0
for a in A:
m = max(m,len(a))
for i,x in enumerate(a):
X[i] = max(X[i],x)
X = [0] + X[:m]
for i in range(m):
X[i+1] += X[i]
return X
def _objStr(s):
if isinstance(s,str):
return s
else:
return str(s)
def _getStr(s):
if isSeq(s):
return list(map(_getStr,s))
else:
return _objStr(s)
def _getLines(s):
if isSeq(s):
return tuple([(x or '').split('\n') for x in s])
else:
return (s or '').split('\n')
def _getLineCount(s):
T = _getLines(s)
if isSeq(s):
return max([len(x) for x in T])
else:
return len(T)
def _getWidths(i,s, fontName, fontSize, subCols):
S = []
aS = S.append
if isSeq(s):
for j,t in enumerate(s):
sc = subCols[j,i]
fN = getattr(sc,'fontName',fontName)
fS = getattr(sc,'fontSize',fontSize)
m = [stringWidth(x, fN, fS) for x in t.split('\n')]
m = max(sc.minWidth,m and max(m) or 0)
aS(m)
aS(sc.rpad)
del S[-1]
else:
sc = subCols[0,i]
fN = getattr(sc,'fontName',fontName)
fS = getattr(sc,'fontSize',fontSize)
m = [stringWidth(x, fN, fS) for x in s.split('\n')]
aS(max(sc.minWidth,m and max(m) or 0))
return S
class SubColProperty(PropHolder):
dividerLines = 0
_attrMap = AttrMap(
minWidth = AttrMapValue(isNumber,desc="minimum width for this subcol"),
rpad = AttrMapValue(isNumber,desc="right padding for this subcol"),
align = AttrMapValue(OneOf('left','right','center','centre','numeric'),desc='alignment in subCol'),
fontName = AttrMapValue(isString, desc="Font name of the strings"),
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
leading = AttrMapValue(isNumber, desc="leading for the strings"),
fillColor = AttrMapValue(isColorOrNone, desc="fontColor"),
underlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="underline definitions"),
overlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="overline definitions"),
dx = AttrMapValue(isNumber, desc="x offset from default position"),
dy = AttrMapValue(isNumber, desc="y offset from default position"),
)
class LegendCallout:
def _legendValues(legend,*args):
'''return a tuple of values from the first function up the stack with isinstance(self,legend)'''
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
return tuple([L[a] for a in args])
_legendValues = staticmethod(_legendValues)
def _selfOrLegendValues(self,legend,*args):
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
return tuple([getattr(self,a,L[a]) for a in args])
def __call__(self,legend,g,thisx,y,colName):
col, name = colName
class LegendSwatchCallout(LegendCallout):
def __call__(self,legend,g,thisx,y,i,colName,swatch):
col, name = colName
class LegendColEndCallout(LegendCallout):
def __call__(self,legend, g, x, xt, y, width, lWidth):
pass
class Legend(Widget):
"""A simple legend containing rectangular swatches and strings.
The swatches are filled rectangles whenever the respective
color object in 'colorNamePairs' is a subclass of Color in
reportlab.lib.colors. Otherwise the object passed instead is
assumed to have 'x', 'y', 'width' and 'height' attributes.
A legend then tries to set them or catches any error. This
lets you plug-in any widget you like as a replacement for
the default rectangular swatches.
Strings can be nicely aligned left or right to the swatches.
"""
_attrMap = AttrMap(
x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"),
y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"),
deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"),
deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"),
dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"),
autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None",advancedUsage=1),
autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None",advancedUsage=1),
yGap = AttrMapValue(isNumber, desc="Additional gap between rows",advancedUsage=1),
dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"),
dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"),
columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"),
alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"),
colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"),
fontName = AttrMapValue(isString, desc="Font name of the strings"),
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
fillColor = AttrMapValue(isColorOrNone, desc="swatches filling color"),
strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"),
strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"),
swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ...",advancedUsage=1),
callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))",advancedUsage=1),
boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'),
variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)',advancedUsage=1),
dividerLines = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom',advancedUsage=1),
dividerWidth = AttrMapValue(isNumber, desc="dividerLines width",advancedUsage=1),
dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color",advancedUsage=1),
dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.',advancedUsage=1),
dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets',advancedUsage=1),
dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset",advancedUsage=1),
colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)",advancedUsage=1),
subCols = AttrMapValue(None,desc="subColumn properties"),
swatchCallout = AttrMapValue(None, desc="a user swatch callout(self,g,x,y,i,(col,name),swatch)",advancedUsage=1),
swdx = AttrMapValue(isNumber, desc="x position adjustment for the swatch"),
swdy = AttrMapValue(isNumber, desc="y position adjustment for the swatch"),
)
def __init__(self):
# Upper-left reference point.
self.x = 0
self.y = 0
# Alginment of text with respect to swatches.
self.alignment = "left"
# x- and y-distances between neighbouring swatches.
self.deltax = 75
self.deltay = 20
self.autoXPadding = 5
self.autoYPadding = 2
# Size of swatch rectangle.
self.dx = 10
self.dy = 10
self.swdx = 0
self.swdy = 0
# Distance between swatch rectangle and text.
self.dxTextSpace = 10
# Max. number of items per column.
self.columnMaximum = 3
# Color/name pairs.
self.colorNamePairs = [ (colors.red, "red"),
(colors.blue, "blue"),
(colors.green, "green"),
(colors.pink, "pink"),
(colors.yellow, "yellow") ]
# Font name and size of the labels.
self.fontName = STATE_DEFAULTS['fontName']
self.fontSize = STATE_DEFAULTS['fontSize']
self.fillColor = STATE_DEFAULTS['fillColor']
self.strokeColor = STATE_DEFAULTS['strokeColor']
self.strokeWidth = STATE_DEFAULTS['strokeWidth']
self.swatchMarker = None
self.boxAnchor = 'nw'
self.yGap = 0
self.variColumn = 0
self.dividerLines = 0
self.dividerWidth = 0.5
self.dividerDashArray = None
self.dividerColor = colors.black
self.dividerOffsX = (0,0)
self.dividerOffsY = 0
self.colEndCallout = None
self._init_subCols()
def _init_subCols(self):
sc = self.subCols = TypedPropertyCollection(SubColProperty)
sc.rpad = 1
sc.dx = sc.dy = sc.minWidth = 0
sc.align = 'right'
sc[0].align = 'left'
def _getChartStyleName(self,chart):
for a in 'lines', 'bars', 'slices', 'strands':
if hasattr(chart,a): return a
return None
def _getChartStyle(self,chart):
return getattr(chart,self._getChartStyleName(chart),None)
def _getTexts(self,colorNamePairs):
if not isAuto(colorNamePairs):
texts = [_getStr(p[1]) for p in colorNamePairs]
else:
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
texts = [chart.getSeriesName(i,'series %d' % i) for i in range(chart._seriesCount)]
return texts
def _calculateMaxBoundaries(self, colorNamePairs):
"Calculate the maximum width of some given strings."
fontName = self.fontName
fontSize = self.fontSize
subCols = self.subCols
M = [_getWidths(i, m, fontName, fontSize, subCols) for i,m in enumerate(self._getTexts(colorNamePairs))]
if not M:
return [0,0]
n = max([len(m) for m in M])
if self.variColumn:
columnMaximum = self.columnMaximum
return [_transMax(n,M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)]
else:
return _transMax(n,M)
def _calcHeight(self):
dy = self.dy
yGap = self.yGap
thisy = upperlefty = self.y - dy
fontSize = self.fontSize
ascent=getFont(self.fontName).face.ascent/1000.
if ascent==0: ascent=0.718 # default (from helvetica)
ascent *= fontSize
leading = fontSize*1.2
deltay = self.deltay
if not deltay: deltay = max(dy,leading)+self.autoYPadding
columnCount = 0
count = 0
lowy = upperlefty
lim = self.columnMaximum - 1
for name in self._getTexts(self.colorNamePairs):
y0 = thisy+(dy-ascent)*0.5
y = y0 - _getLineCount(name)*leading
leadingMove = 2*y0-y-thisy
newy = thisy-max(deltay,leadingMove)-yGap
lowy = min(y,newy,lowy)
if count==lim:
count = 0
thisy = upperlefty
columnCount = columnCount + 1
else:
thisy = newy
count = count+1
return upperlefty - lowy
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
return Rect(x, thisy, dx, dy,
fillColor = fillColor,
strokeColor = strokeColor,
strokeWidth = strokeWidth,
)
def draw(self):
colorNamePairs = self.colorNamePairs
autoCP = isAuto(colorNamePairs)
if autoCP:
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
swatchMarker = None
autoCP = Auto(obj=chart)
n = chart._seriesCount
chartTexts = self._getTexts(colorNamePairs)
else:
swatchMarker = getattr(self,'swatchMarker',None)
if isAuto(swatchMarker):
chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None))
swatchMarker = Auto(obj=chart)
n = len(colorNamePairs)
dx = self.dx
dy = self.dy
alignment = self.alignment
columnMaximum = self.columnMaximum
deltax = self.deltax
deltay = self.deltay
dxTextSpace = self.dxTextSpace
fontName = self.fontName
fontSize = self.fontSize
fillColor = self.fillColor
strokeWidth = self.strokeWidth
strokeColor = self.strokeColor
subCols = self.subCols
leading = fontSize*1.2
yGap = self.yGap
if not deltay:
deltay = max(dy,leading)+self.autoYPadding
ba = self.boxAnchor
maxWidth = self._calculateMaxBoundaries(colorNamePairs)
nCols = int((n+columnMaximum-1)/(columnMaximum*1.0))
xW = dx+dxTextSpace+self.autoXPadding
variColumn = self.variColumn
if variColumn:
width = reduce(operator.add,[m[-1] for m in maxWidth],0)+xW*nCols
else:
deltax = max(maxWidth[-1]+xW,deltax)
width = maxWidth[-1]+nCols*deltax
maxWidth = nCols*[maxWidth]
thisx = self.x
thisy = self.y - self.dy
if ba not in ('ne','n','nw','autoy'):
height = self._calcHeight()
if ba in ('e','c','w'):
thisy += height/2.
else:
thisy += height
if ba not in ('nw','w','sw','autox'):
if ba in ('n','c','s'):
thisx -= width/2
else:
thisx -= width
upperlefty = thisy
g = Group()
ascent=getFont(fontName).face.ascent/1000.
if ascent==0: ascent=0.718 # default (from helvetica)
ascent *= fontSize # normalize
lim = columnMaximum - 1
callout = getattr(self,'callout',None)
scallout = getattr(self,'swatchCallout',None)
dividerLines = self.dividerLines
if dividerLines:
dividerWidth = self.dividerWidth
dividerColor = self.dividerColor
dividerDashArray = self.dividerDashArray
dividerOffsX = self.dividerOffsX
dividerOffsY = self.dividerOffsY
for i in range(n):
if autoCP:
col = autoCP
col.index = i
name = chartTexts[i]
else:
col, name = colorNamePairs[i]
if isAuto(swatchMarker):
col = swatchMarker
col.index = i
if isAuto(name):
name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i)
T = _getLines(name)
S = []
aS = S.append
j = int(i/(columnMaximum*1.0))
jOffs = maxWidth[j]
# thisy+dy/2 = y+leading/2
y = y0 = thisy+(dy-ascent)*0.5
if callout: callout(self,g,thisx,y,(col,name))
if alignment == "left":
x = thisx
xn = thisx+jOffs[-1]+dxTextSpace
elif alignment == "right":
x = thisx+dx+dxTextSpace
xn = thisx
else:
raise ValueError("bad alignment")
if not isSeq(name):
T = [T]
yd = y
for k,lines in enumerate(T):
y = y0
kk = k*2
x1 = x+jOffs[kk]
x2 = x+jOffs[kk+1]
sc = subCols[k,i]
anchor = sc.align
scdx = sc.dx
scdy = sc.dy
fN = getattr(sc,'fontName',fontName)
fS = getattr(sc,'fontSize',fontSize)
fC = getattr(sc,'fillColor',fillColor)
fL = getattr(sc,'leading',1.2*fontSize)
if fN==fontName:
fA = (ascent*fS)/fontSize
else:
fA = getFont(fontName).face.ascent/1000.
if fA==0: fA=0.718
fA *= fS
if anchor=='left':
anchor = 'start'
xoffs = x1
elif anchor=='right':
anchor = 'end'
xoffs = x2
elif anchor=='numeric':
xoffs = x2
else:
anchor = 'middle'
xoffs = 0.5*(x1+x2)
for t in lines:
aS(String(xoffs+scdx,y+scdy,t,fontName=fN,fontSize=fS,fillColor=fC, textAnchor = anchor))
y -= fL
yd = min(yd,y)
y += fL
for iy, a in ((y-max(fL-fA,0),'underlines'),(y+fA,'overlines')):
il = getattr(sc,a,None)
if il:
if not isinstance(il,(tuple,list)): il = (il,)
for l in il:
l = copy.copy(l)
l.y1 += iy
l.y2 += iy
l.x1 += x1
l.x2 += x2
aS(l)
x = xn
y = yd
leadingMove = 2*y0-y-thisy
if dividerLines:
xd = thisx+dx+dxTextSpace+jOffs[-1]+dividerOffsX[1]
yd = thisy+dy*0.5+dividerOffsY
if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum):
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)):
yd -= max(deltay,leadingMove)+yGap
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
# Make a 'normal' color swatch...
swatchX = x + getattr(self,'swdx',0)
swatchY = thisy + getattr(self,'swdy',0)
if isAuto(col):
chart = getattr(col,'chart',getattr(col,'obj',None))
c = chart.makeSwatchSample(getattr(col,'index',i),swatchX,swatchY,dx,dy)
elif isinstance(col, colors.Color):
if isSymbol(swatchMarker):
c = uSymbol2Symbol(swatchMarker,swatchX+dx/2.,swatchY+dy/2.,col)
else:
c = self._defaultSwatch(swatchX,swatchY,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)
elif col is not None:
try:
c = copy.deepcopy(col)
c.x = swatchX
c.y = swatchY
c.width = dx
c.height = dy
except:
c = None
else:
c = None
if c:
g.add(c)
if scallout: scallout(self,g,thisx,y0,i,(col,name),c)
for s in S: g.add(s)
if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)):
if alignment == "left":
xt = thisx
else:
xt = thisx+dx+dxTextSpace
yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap)
self.colEndCallout(self, g, thisx, xt, yd, jOffs[-1], jOffs[-1]+dx+dxTextSpace)
if i%columnMaximum==lim:
if variColumn:
thisx += jOffs[-1]+xW
else:
thisx = thisx+deltax
thisy = upperlefty
else:
thisy = thisy-max(deltay,leadingMove)-yGap
return g
def demo(self):
"Make sample legend."
d = Drawing(200, 100)
legend = Legend()
legend.alignment = 'left'
legend.x = 0
legend.y = 100
legend.dxTextSpace = 5
items = 'red green blue yellow pink black white'.split()
items = [(getattr(colors, i), i) for i in items]
legend.colorNamePairs = items
d.add(legend, 'legend')
return d
class TotalAnnotator(LegendColEndCallout):
def __init__(self, lText='Total', rText='0.0', fontName=_baseGFontName, fontSize=10,
fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None,
dx=0, dy=0, dly=0, dlx=(0,0)):
self.lText = lText
self.rText = rText
self.fontName = fontName
self.fontSize = fontSize
self.fillColor = fillColor
self.dy = dy
self.dx = dx
self.dly = dly
self.dlx = dlx
self.strokeWidth = strokeWidth
self.strokeColor = strokeColor
self.strokeDashArray = strokeDashArray
def __call__(self,legend, g, x, xt, y, width, lWidth):
from reportlab.graphics.shapes import String, Line
fontSize = self.fontSize
fontName = self.fontName
fillColor = self.fillColor
strokeColor = self.strokeColor
strokeWidth = self.strokeWidth
ascent=getFont(fontName).face.ascent/1000.
if ascent==0: ascent=0.718 # default (from helvetica)
ascent *= fontSize
leading = fontSize*1.2
yt = y+self.dy-ascent*1.3
if self.lText and fillColor:
g.add(String(xt,yt,self.lText,
fontName=fontName,
fontSize=fontSize,
fillColor=fillColor,
textAnchor = "start"))
if self.rText:
g.add(String(xt+width,yt,self.rText,
fontName=fontName,
fontSize=fontSize,
fillColor=fillColor,
textAnchor = "end"))
if strokeWidth and strokeColor:
yL = y+self.dly-leading
g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL,
strokeColor=strokeColor, strokeWidth=strokeWidth,
strokeDashArray=self.strokeDashArray))
class LineSwatch(Widget):
"""basically a Line with properties added so it can be used in a LineLegend"""
_attrMap = AttrMap(
x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"),
y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"),
width = AttrMapValue(isNumber, desc="length of swatch line"),
height = AttrMapValue(isNumber, desc="used for line strokeWidth"),
strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"),
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"),
)
def __init__(self):
from reportlab.lib.colors import red
from reportlab.graphics.shapes import Line
self.x = 0
self.y = 0
self.width = 20
self.height = 1
self.strokeColor = red
self.strokeDashArray = None
def draw(self):
l = Line(self.x,self.y,self.x+self.width,self.y)
l.strokeColor = self.strokeColor
l.strokeDashArray = self.strokeDashArray
l.strokeWidth = self.height
return l
class LineLegend(Legend):
"""A subclass of Legend for drawing legends with lines as the
swatches rather than rectangles. Useful for lineCharts and
linePlots. Should be similar in all other ways the the standard
Legend class.
"""
def __init__(self):
Legend.__init__(self)
# Size of swatch rectangle.
self.dx = 10
self.dy = 2
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
l = LineSwatch()
l.x = x
l.y = thisy
l.width = dx
l.height = dy
l.strokeColor = fillColor
return l
def sample1c():
"Make sample legend."
d = Drawing(200, 100)
legend = Legend()
legend.alignment = 'right'
legend.x = 0
legend.y = 100
legend.dxTextSpace = 5
items = 'red green blue yellow pink black white'.split()
items = [(getattr(colors, i), i) for i in items]
legend.colorNamePairs = items
d.add(legend, 'legend')
return d
def sample2c():
"Make sample legend."
d = Drawing(200, 100)
legend = Legend()
legend.alignment = 'right'
legend.x = 20
legend.y = 90
legend.deltax = 60
legend.dxTextSpace = 10
legend.columnMaximum = 4
items = 'red green blue yellow pink black white'.split()
items = [(getattr(colors, i), i) for i in items]
legend.colorNamePairs = items
d.add(legend, 'legend')
return d
def sample3():
"Make sample legend with line swatches."
d = Drawing(200, 100)
legend = LineLegend()
legend.alignment = 'right'
legend.x = 20
legend.y = 90
legend.deltax = 60
legend.dxTextSpace = 10
legend.columnMaximum = 4
items = 'red green blue yellow pink black white'.split()
items = [(getattr(colors, i), i) for i in items]
legend.colorNamePairs = items
d.add(legend, 'legend')
return d
def sample3a():
"Make sample legend with line swatches and dasharrays on the lines."
d = Drawing(200, 100)
legend = LineLegend()
legend.alignment = 'right'
legend.x = 20
legend.y = 90
legend.deltax = 60
legend.dxTextSpace = 10
legend.columnMaximum = 4
items = 'red green blue yellow pink black white'.split()
darrays = ([2,1], [2,5], [2,2,5,5], [1,2,3,4], [4,2,3,4], [1,2,3,4,5,6], [1])
cnp = []
for i in range(0, len(items)):
l = LineSwatch()
l.strokeColor = getattr(colors, items[i])
l.strokeDashArray = darrays[i]
cnp.append((l, items[i]))
legend.colorNamePairs = cnp
d.add(legend, 'legend')
return d

View File

@@ -0,0 +1,715 @@
#Copyright ReportLab Europe Ltd. 2000-2004
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/linecharts.py
__version__=''' $Id$ '''
__doc__="""This modules defines a very preliminary Line Chart example."""
from reportlab.lib import colors
from reportlab.lib.validators import isNumber, isNumberOrNone, isColor, isColorOrNone, isListOfStrings, \
isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \
isListOfNumbersOrNone, isStringOrNone, OneOf, Percentage
from reportlab.lib.attrmap import *
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine
from reportlab.graphics.widgets.signsandsymbols import NoEntry
from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker
from reportlab.graphics.charts.areas import PlotArea
from reportlab.graphics.charts.legends import _objStr
class LineChartProperties(PropHolder):
_attrMap = AttrMap(
strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'),
strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line or border.'),
fillColor = AttrMapValue(isColorOrNone, desc='fill color of a bar.'),
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'),
symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.',advancedUsage=1),
shader = AttrMapValue(None, desc='Shader Class.',advancedUsage=1),
filler = AttrMapValue(None, desc='Filler Class.',advancedUsage=1),
name = AttrMapValue(isStringOrNone, desc='Name of the line.'),
lineStyle = AttrMapValue(NoneOr(OneOf('line','joinedLine','bar')), desc="What kind of plot this line is",advancedUsage=1),
barWidth = AttrMapValue(isNumberOrNone,desc="Percentage of available width to be used for a bar",advancedUsage=1),
)
class AbstractLineChart(PlotArea):
def makeSwatchSample(self,rowNo, x, y, width, height):
baseStyle = self.lines
styleIdx = rowNo % len(baseStyle)
style = baseStyle[styleIdx]
color = style.strokeColor
yh2 = y+height/2.
lineStyle = getattr(style,'lineStyle',None)
if lineStyle=='bar':
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
L = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=color,strokeLineCap=0,strokeDashArray=dash,fillColor=getattr(style,'fillColor',color))
elif self.joinedLines or lineStyle=='joinedLine':
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
L = Line(x,yh2,x+width,yh2,strokeColor=color,strokeLineCap=0)
if strokeWidth: L.strokeWidth = strokeWidth
if dash: L.strokeDashArray = dash
else:
L = None
if hasattr(style, 'symbol'):
S = style.symbol
elif hasattr(baseStyle, 'symbol'):
S = baseStyle.symbol
else:
S = None
if S: S = uSymbol2Symbol(S,x+width/2.,yh2,color)
if S and L:
g = Group()
g.add(L)
g.add(S)
return g
return S or L
def getSeriesName(self,i,default=None):
'''return series name i or default'''
return _objStr(getattr(self.lines[i],'name',default))
class LineChart(AbstractLineChart):
pass
# This is conceptually similar to the VerticalBarChart.
# Still it is better named HorizontalLineChart... :-/
class HorizontalLineChart(LineChart):
"""Line chart with multiple lines.
A line chart is assumed to have one category and one value axis.
Despite its generic name this particular line chart class has
a vertical value axis and a horizontal category one. It may
evolve into individual horizontal and vertical variants (like
with the existing bar charts).
Available attributes are:
x: x-position of lower-left chart origin
y: y-position of lower-left chart origin
width: chart width
height: chart height
useAbsolute: disables auto-scaling of chart elements (?)
lineLabelNudge: distance of data labels to data points
lineLabels: labels associated with data values
lineLabelFormat: format string or callback function
groupSpacing: space between categories
joinedLines: enables drawing of lines
strokeColor: color of chart lines (?)
fillColor: color for chart background (?)
lines: style list, used cyclically for data series
valueAxis: value axis object
categoryAxis: category axis object
categoryNames: category names
data: chart data, a list of data series of equal length
"""
_attrMap = AttrMap(BASE=LineChart,
useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.',advancedUsage=1),
lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
lines = AttrMapValue(None, desc='Handle of the lines.'),
valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.',advancedUsage=1),
reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
)
def __init__(self):
LineChart.__init__(self)
# Allow for a bounding rectangle.
self.strokeColor = None
self.fillColor = None
# Named so we have less recoding for the horizontal one :-)
self.categoryAxis = XCategoryAxis()
self.valueAxis = YValueAxis()
# This defines two series of 3 points. Just an example.
self.data = [(100,110,120,130),
(70, 80, 80, 90)]
self.categoryNames = ('North','South','East','West')
self.lines = TypedPropertyCollection(LineChartProperties)
self.lines.strokeWidth = 1
self.lines[0].strokeColor = colors.red
self.lines[1].strokeColor = colors.green
self.lines[2].strokeColor = colors.blue
# control spacing. if useAbsolute = 1 then
# the next parameters are in points; otherwise
# they are 'proportions' and are normalized to
# fit the available space.
self.useAbsolute = 0 #- not done yet
self.groupSpacing = 1 #5
self.lineLabels = TypedPropertyCollection(Label)
self.lineLabelFormat = None
self.lineLabelArray = None
# This says whether the origin is above or below
# the data point. +10 means put the origin ten points
# above the data point if value > 0, or ten
# points below if data value < 0. This is different
# to label dx/dy which are not dependent on the
# sign of the data.
self.lineLabelNudge = 10
# If you have multiple series, by default they butt
# together.
# New line chart attributes.
self.joinedLines = 1 # Connect items with straight lines.
self.inFill = 0
self.reversePlotOrder = 0
def demo(self):
"""Shows basic use of a line chart."""
drawing = Drawing(200, 100)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(14, 10, 21, 28, 38, 46, 25, 5)
]
lc = HorizontalLineChart()
lc.x = 20
lc.y = 10
lc.height = 85
lc.width = 170
lc.data = data
lc.lines.symbol = makeMarker('Circle')
drawing.add(lc)
return drawing
def calcPositions(self):
"""Works out where they go.
Sets an attribute _positions which is a list of
lists of (x, y) matching the data.
"""
self._seriesCount = len(self.data)
self._rowLength = max(list(map(len,self.data)))
if self.useAbsolute:
# Dimensions are absolute.
normFactor = 1.0
else:
# Dimensions are normalized to fit.
normWidth = self.groupSpacing
availWidth = self.categoryAxis.scale(0)[1]
normFactor = availWidth / normWidth
self._normFactor = normFactor
self._yzero = yzero = self.valueAxis.scale(0)
self._hngs = hngs = 0.5 * self.groupSpacing * normFactor
self._positions = []
for rowNo in range(len(self.data)):
lineRow = []
for colNo in range(len(self.data[rowNo])):
datum = self.data[rowNo][colNo]
if datum is not None:
(groupX, groupWidth) = self.categoryAxis.scale(colNo)
x = groupX + hngs
y = yzero
height = self.valueAxis.scale(datum) - y
lineRow.append((x, y+height))
self._positions.append(lineRow)
def _innerDrawLabel(self, rowNo, colNo, x, y):
"Draw a label for a given item in the list."
labelFmt = self.lineLabelFormat
labelValue = self.data[rowNo][colNo]
if labelFmt is None:
labelText = None
elif type(labelFmt) is str:
if labelFmt == 'values':
try:
labelText = self.lineLabelArray[rowNo][colNo]
except:
labelText = None
else:
labelText = labelFmt % labelValue
elif hasattr(labelFmt,'__call__'):
labelText = labelFmt(labelValue)
else:
raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)
if labelText:
label = self.lineLabels[(rowNo, colNo)]
if not label.visible: return
# Make sure labels are some distance off the data point.
if y > 0:
label.setOrigin(x, y + self.lineLabelNudge)
else:
label.setOrigin(x, y - self.lineLabelNudge)
label.setText(labelText)
else:
label = None
return label
def drawLabel(self, G, rowNo, colNo, x, y):
'''Draw a label for a given item in the list.
G must have an add method'''
G.add(self._innerDrawLabel(rowNo,colNo,x,y))
def makeLines(self):
g = Group()
labelFmt = self.lineLabelFormat
P = list(range(len(self._positions)))
if self.reversePlotOrder: P.reverse()
inFill = self.inFill
if inFill:
inFillY = self.categoryAxis._y
inFillX0 = self.valueAxis._x
inFillX1 = inFillX0 + self.categoryAxis._length
inFillG = getattr(self,'_inFillG',g)
yzero = self._yzero
# Iterate over data rows.
for rowNo in P:
row = self._positions[rowNo]
styleCount = len(self.lines)
styleIdx = rowNo % styleCount
rowStyle = self.lines[styleIdx]
rowColor = rowStyle.strokeColor
dash = getattr(rowStyle, 'strokeDashArray', None)
lineStyle = getattr(rowStyle,'lineStyle',None)
if hasattr(rowStyle, 'strokeWidth'):
strokeWidth = rowStyle.strokeWidth
elif hasattr(self.lines, 'strokeWidth'):
strokeWidth = self.lines.strokeWidth
else:
strokeWidth = None
# Iterate over data columns.
if lineStyle=='bar':
barWidth = getattr(rowStyle,'barWidth',Percentage(50))
fillColor = getattr(rowStyle,'fillColor',rowColor)
if isinstance(barWidth,Percentage):
hbw = self._hngs*barWidth*0.01
else:
hbw = barWidth*0.5
for colNo in range(len(row)):
x,y = row[colNo]
g.add(Rect(x-hbw,min(y,yzero),2*hbw,abs(y-yzero),strokeWidth=strokeWidth,strokeColor=rowColor,fillColor=fillColor))
elif self.joinedLines or lineStyle=='joinedLine':
points = []
for colNo in range(len(row)):
points += row[colNo]
if inFill:
points = points + [inFillX1,inFillY,inFillX0,inFillY]
inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1))
else:
line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
if strokeWidth:
line.strokeWidth = strokeWidth
if dash:
line.strokeDashArray = dash
g.add(line)
if hasattr(rowStyle, 'symbol'):
uSymbol = rowStyle.symbol
elif hasattr(self.lines, 'symbol'):
uSymbol = self.lines.symbol
else:
uSymbol = None
if uSymbol:
for colNo in range(len(row)):
x1, y1 = row[colNo]
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor)
if symbol: g.add(symbol)
# Draw item labels.
for colNo in range(len(row)):
x1, y1 = row[colNo]
self.drawLabel(g, rowNo, colNo, x1, y1)
return g
def draw(self):
"Draws itself."
vA, cA = self.valueAxis, self.categoryAxis
vA.setPosition(self.x, self.y, self.height)
if vA: vA.joinAxis = cA
if cA: cA.joinAxis = vA
vA.configure(self.data)
# If zero is in chart, put x axis there, otherwise
# use bottom.
xAxisCrossesAt = vA.scale(0)
if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
y = self.y
else:
y = xAxisCrossesAt
cA.setPosition(self.x, y, self.width)
cA.configure(self.data)
self.calcPositions()
g = Group()
g.add(self.makeBackground())
if self.inFill:
self._inFillG = Group()
g.add(self._inFillG)
g.add(cA)
g.add(vA)
cAdgl = getattr(cA,'drawGridLast',False)
vAdgl = getattr(vA,'drawGridLast',False)
if not cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
if not vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
g.add(self.makeLines())
if cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
if vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
return g
def _fakeItemKey(a):
'''t, z0, z1, x, y = a[:5]'''
return (-a[1],a[3],a[0],-a[4])
class _FakeGroup:
def __init__(self):
self._data = []
def add(self,what):
if what: self._data.append(what)
def value(self):
return self._data
def sort(self):
self._data.sort(key=_fakeItemKey)
#for t in self._data: print t
class HorizontalLineChart3D(HorizontalLineChart):
_attrMap = AttrMap(BASE=HorizontalLineChart,
theta_x = AttrMapValue(isNumber, desc='dx/dz'),
theta_y = AttrMapValue(isNumber, desc='dy/dz'),
zDepth = AttrMapValue(isNumber, desc='depth of an individual series'),
zSpace = AttrMapValue(isNumber, desc='z gap around series'),
)
theta_x = .5
theta_y = .5
zDepth = 10
zSpace = 3
def calcPositions(self):
HorizontalLineChart.calcPositions(self)
nSeries = self._seriesCount
zSpace = self.zSpace
zDepth = self.zDepth
if self.categoryAxis.style=='parallel_3d':
_3d_depth = nSeries*zDepth+(nSeries+1)*zSpace
else:
_3d_depth = zDepth + 2*zSpace
self._3d_dx = self.theta_x*_3d_depth
self._3d_dy = self.theta_y*_3d_depth
def _calc_z0(self,rowNo):
zSpace = self.zSpace
if self.categoryAxis.style=='parallel_3d':
z0 = rowNo*(self.zDepth+zSpace)+zSpace
else:
z0 = zSpace
return z0
def _zadjust(self,x,y,z):
return x+z*self.theta_x, y+z*self.theta_y
def makeLines(self):
labelFmt = self.lineLabelFormat
P = list(range(len(self._positions)))
if self.reversePlotOrder: P.reverse()
inFill = self.inFill
assert not inFill, "inFill not supported for 3d yet"
#if inFill:
#inFillY = self.categoryAxis._y
#inFillX0 = self.valueAxis._x
#inFillX1 = inFillX0 + self.categoryAxis._length
#inFillG = getattr(self,'_inFillG',g)
zDepth = self.zDepth
_zadjust = self._zadjust
theta_x = self.theta_x
theta_y = self.theta_y
F = _FakeGroup()
from reportlab.graphics.charts.utils3d import _make_3d_line_info
tileWidth = getattr(self,'_3d_tilewidth',None)
if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1
# Iterate over data rows.
for rowNo in P:
row = self._positions[rowNo]
n = len(row)
styleCount = len(self.lines)
styleIdx = rowNo % styleCount
rowStyle = self.lines[styleIdx]
rowColor = rowStyle.strokeColor
dash = getattr(rowStyle, 'strokeDashArray', None)
z0 = self._calc_z0(rowNo)
z1 = z0 + zDepth
if hasattr(self.lines[styleIdx], 'strokeWidth'):
strokeWidth = self.lines[styleIdx].strokeWidth
elif hasattr(self.lines, 'strokeWidth'):
strokeWidth = self.lines.strokeWidth
else:
strokeWidth = None
# Iterate over data columns.
if self.joinedLines:
if n:
x0, y0 = row[0]
for colNo in range(1,n):
x1, y1 = row[colNo]
_make_3d_line_info( F, x0, x1, y0, y1, z0, z1,
theta_x, theta_y,
rowColor, fillColorShaded=None, tileWidth=tileWidth,
strokeColor=None, strokeWidth=None, strokeDashArray=None,
shading=0.1)
x0, y0 = x1, y1
if hasattr(self.lines[styleIdx], 'symbol'):
uSymbol = self.lines[styleIdx].symbol
elif hasattr(self.lines, 'symbol'):
uSymbol = self.lines.symbol
else:
uSymbol = None
if uSymbol:
for colNo in range(n):
x1, y1 = row[colNo]
x1, y1 = _zadjust(x1,y1,z0)
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor)
if symbol: F.add((2,z0,z0,x1,y1,symbol))
# Draw item labels.
for colNo in range(n):
x1, y1 = row[colNo]
x1, y1 = _zadjust(x1,y1,z0)
L = self._innerDrawLabel(rowNo, colNo, x1, y1)
if L: F.add((2,z0,z0,x1,y1,L))
F.sort()
g = Group()
for v in F.value(): g.add(v[-1])
return g
class VerticalLineChart(LineChart):
pass
def sample1():
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(5, 20, 46, 38, 23, 21, 6, 14)
]
lc = HorizontalLineChart()
lc.x = 50
lc.y = 50
lc.height = 125
lc.width = 300
lc.data = data
lc.joinedLines = 1
lc.lines.symbol = makeMarker('FilledDiamond')
lc.lineLabelFormat = '%2.0f'
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
lc.categoryAxis.categoryNames = catNames
lc.categoryAxis.labels.boxAnchor = 'n'
lc.valueAxis.valueMin = 0
lc.valueAxis.valueMax = 60
lc.valueAxis.valueStep = 15
drawing.add(lc)
return drawing
class SampleHorizontalLineChart(HorizontalLineChart):
"Sample class overwriting one method to draw additional horizontal lines."
def demo(self):
"""Shows basic use of a line chart."""
drawing = Drawing(200, 100)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(14, 10, 21, 28, 38, 46, 25, 5)
]
lc = SampleHorizontalLineChart()
lc.x = 20
lc.y = 10
lc.height = 85
lc.width = 170
lc.data = data
lc.strokeColor = colors.white
lc.fillColor = colors.HexColor(0xCCCCCC)
drawing.add(lc)
return drawing
def makeBackground(self):
g = Group()
g.add(HorizontalLineChart.makeBackground(self))
valAxis = self.valueAxis
valTickPositions = valAxis._tickValues
for y in valTickPositions:
y = valAxis.scale(y)
g.add(Line(self.x, y, self.x+self.width, y,
strokeColor = self.strokeColor))
return g
def sample1a():
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(5, 20, 46, 38, 23, 21, 6, 14)
]
lc = SampleHorizontalLineChart()
lc.x = 50
lc.y = 50
lc.height = 125
lc.width = 300
lc.data = data
lc.joinedLines = 1
lc.strokeColor = colors.white
lc.fillColor = colors.HexColor(0xCCCCCC)
lc.lines.symbol = makeMarker('FilledDiamond')
lc.lineLabelFormat = '%2.0f'
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
lc.categoryAxis.categoryNames = catNames
lc.categoryAxis.labels.boxAnchor = 'n'
lc.valueAxis.valueMin = 0
lc.valueAxis.valueMax = 60
lc.valueAxis.valueStep = 15
drawing.add(lc)
return drawing
def sample2():
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(5, 20, 46, 38, 23, 21, 6, 14)
]
lc = HorizontalLineChart()
lc.x = 50
lc.y = 50
lc.height = 125
lc.width = 300
lc.data = data
lc.joinedLines = 1
lc.lines.symbol = makeMarker('Smiley')
lc.lineLabelFormat = '%2.0f'
lc.strokeColor = colors.black
lc.fillColor = colors.lightblue
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
lc.categoryAxis.categoryNames = catNames
lc.categoryAxis.labels.boxAnchor = 'n'
lc.valueAxis.valueMin = 0
lc.valueAxis.valueMax = 60
lc.valueAxis.valueStep = 15
drawing.add(lc)
return drawing
def sample3():
drawing = Drawing(400, 200)
data = [
(13, 5, 20, 22, 37, 45, 19, 4),
(5, 20, 46, 38, 23, 21, 6, 14)
]
lc = HorizontalLineChart()
lc.x = 50
lc.y = 50
lc.height = 125
lc.width = 300
lc.data = data
lc.joinedLines = 1
lc.lineLabelFormat = '%2.0f'
lc.strokeColor = colors.black
lc.lines[0].symbol = makeMarker('Smiley')
lc.lines[1].symbol = NoEntry
lc.lines[0].strokeWidth = 2
lc.lines[1].strokeWidth = 4
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
lc.categoryAxis.categoryNames = catNames
lc.categoryAxis.labels.boxAnchor = 'n'
lc.valueAxis.valueMin = 0
lc.valueAxis.valueMax = 60
lc.valueAxis.valueStep = 15
drawing.add(lc)
return drawing

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/markers.py
__version__=''' $Id$ '''
__doc__="""This modules defines a collection of markers used in charts.
The make* functions return a simple shape or a widget as for
the smiley.
"""
from reportlab.lib import colors
from reportlab.graphics.shapes import Rect, Line, Circle, Polygon
from reportlab.graphics.widgets.signsandsymbols import SmileyFace
def makeEmptySquare(x, y, size, color):
"Make an empty square marker."
d = size/2.0
rect = Rect(x-d, y-d, 2*d, 2*d)
rect.strokeColor = color
rect.fillColor = None
return rect
def makeFilledSquare(x, y, size, color):
"Make a filled square marker."
d = size/2.0
rect = Rect(x-d, y-d, 2*d, 2*d)
rect.strokeColor = color
rect.fillColor = color
return rect
def makeFilledDiamond(x, y, size, color):
"Make a filled diamond marker."
d = size/2.0
poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d))
poly.strokeColor = color
poly.fillColor = color
return poly
def makeEmptyCircle(x, y, size, color):
"Make a hollow circle marker."
d = size/2.0
circle = Circle(x, y, d)
circle.strokeColor = color
circle.fillColor = colors.white
return circle
def makeFilledCircle(x, y, size, color):
"Make a hollow circle marker."
d = size/2.0
circle = Circle(x, y, d)
circle.strokeColor = color
circle.fillColor = color
return circle
def makeSmiley(x, y, size, color):
"Make a smiley marker."
d = size
s = SmileyFace()
s.fillColor = color
s.x = x-d
s.y = y-d
s.size = d*2
return s

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
from reportlab.lib.colors import Color, white, black
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.shapes import Polygon, Line, Circle, String, Drawing, PolyLine, Group, Rect
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection
from reportlab.lib.attrmap import *
from reportlab.lib.validators import *
from reportlab.lib.units import cm
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
from reportlab.graphics.widgets.grids import ShadedRect, Grid
class SlideBox(Widget):
"""Returns a slidebox widget"""
_attrMap = AttrMap(
labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"),
labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"),
labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"),
labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"),
startColor = AttrMapValue(isColor, desc='Color of first box'),
endColor = AttrMapValue(isColor, desc='Color of last box'),
numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'),
trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'),
triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"),
triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"),
triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"),
triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"),
triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"),
boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"),
boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"),
boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"),
boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"),
boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"),
leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'),
rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'),
topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'),
bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'),
sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"),
sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"),
sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"),
sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"),
)
def __init__(self):
self.labelFontName = "Helvetica-Bold"
self.labelFontSize = 10
self.labelStrokeColor = black
self.labelFillColor = white
self.startColor = colors.Color(232/255.0,224/255.0,119/255.0)
self.endColor = colors.Color(25/255.0,77/255.0,135/255.0)
self.numberOfBoxes = 7
self.trianglePosition = 7
self.triangleHeight = 0.12*cm
self.triangleWidth = 0.38*cm
self.triangleFillColor = white
self.triangleStrokeColor = black
self.triangleStrokeWidth = 0.58
self.boxHeight = 0.55*cm
self.boxWidth = 0.73*cm
self.boxSpacing = 0.075*cm
self.boxOutlineColor = black
self.boxOutlineWidth = 0.58
self.leftPadding=5
self.rightPadding=5
self.topPadding=5
self.bottomPadding=5
self.background=None
self.sourceLabelText = "Source: ReportLab"
self.sourceLabelOffset = 0.2*cm
self.sourceLabelFontName = "Helvetica-Oblique"
self.sourceLabelFontSize = 6
self.sourceLabelFillColor = black
def _getDrawingDimensions(self):
tx=(self.numberOfBoxes*self.boxWidth)
if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing)
tx=tx+self.leftPadding+self.rightPadding
ty=self.boxHeight+self.triangleHeight
ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize
return (tx,ty)
def _getColors(self):
# for calculating intermediate colors...
numShades = self.numberOfBoxes+1
fillColorStart = self.startColor
fillColorEnd = self.endColor
colorsList =[]
for i in range(0,numShades):
colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i))
return colorsList
def demo(self,drawing=None):
from reportlab.lib import colors
if not drawing:
tx,ty=self._getDrawingDimensions()
drawing = Drawing(tx,ty)
drawing.add(self.draw())
return drawing
def draw(self):
g = Group()
ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize
if self.background:
x,y = self._getDrawingDimensions()
g.add(Rect(-self.leftPadding,-ys,x,y,
strokeColor=None,
strokeWidth=0,
fillColor=self.background))
ascent=getFont(self.labelFontName).face.ascent/1000.
if ascent==0: ascent=0.718 # default (from helvetica)
ascent=ascent*self.labelFontSize # normalize
colorsList = self._getColors()
# Draw the boxes - now uses ShadedRect from grids
x=0
for f in range (0,self.numberOfBoxes):
sr=ShadedRect()
sr.x=x
sr.y=0
sr.width=self.boxWidth
sr.height=self.boxHeight
sr.orientation = 'vertical'
sr.numShades = 30
sr.fillColorStart = colorsList[f]
sr.fillColorEnd = colorsList[f+1]
sr.strokeColor = None
sr.strokeWidth = 0
g.add(sr)
g.add(Rect(x,0,self.boxWidth,self.boxHeight,
strokeColor=self.boxOutlineColor,
strokeWidth=self.boxOutlineWidth,
fillColor=None))
g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2.,
text = str(f+1),
fillColor = self.labelFillColor,
strokeColor=self.labelStrokeColor,
textAnchor = 'middle',
fontName = self.labelFontName,
fontSize = self.labelFontSize))
x=x+self.boxWidth+self.boxSpacing
#do triangles
xt = (self.trianglePosition*self.boxWidth)
if self.trianglePosition>1:
xt = xt+(self.trianglePosition-1)*self.boxSpacing
xt = xt-(self.boxWidth/2)
g.add(Polygon(
strokeColor = self.triangleStrokeColor,
strokeWidth = self.triangleStrokeWidth,
fillColor = self.triangleFillColor,
points=[xt,self.boxHeight-(self.triangleHeight/2),
xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
xt,self.boxHeight-(self.triangleHeight/2)]))
g.add(Polygon(
strokeColor = self.triangleStrokeColor,
strokeWidth = self.triangleStrokeWidth,
fillColor = self.triangleFillColor,
points=[xt,0+(self.triangleHeight/2),
xt-(self.triangleWidth/2),0-(self.triangleHeight/2),
xt+(self.triangleWidth/2),0-(self.triangleHeight/2),
xt,0+(self.triangleHeight/2)]))
#source label
if self.sourceLabelText != None:
g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize),
text = self.sourceLabelText,
fillColor = self.sourceLabelFillColor,
textAnchor = 'end',
fontName = self.sourceLabelFontName,
fontSize = self.sourceLabelFontSize))
g.shift(self.leftPadding, ys)
return g
if __name__ == "__main__":
d = SlideBox()
d.demo().save(fnRoot="slidebox")

View File

@@ -0,0 +1,408 @@
#Copyright ReportLab Europe Ltd. 2000-2004
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/spider.py
# spider chart, also known as radar chart
__version__=''' $Id$ '''
__doc__="""Spider Chart
Normal use shows variation of 5-10 parameters against some 'norm' or target.
When there is more than one series, place the series with the largest
numbers first, as it will be overdrawn by each successive one.
"""
import copy
from math import sin, cos, pi
from reportlab.lib import colors
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
isListOfNumbers, isColorOrNone, isString,\
isListOfStringsOrNone, OneOf, SequenceOf,\
isBoolean, isListOfColors, isNumberOrNone,\
isNoneOrListOfNoneOrStrings, isTextAnchor,\
isNoneOrListOfNoneOrNumbers, isBoxAnchor,\
isStringOrNone, isStringOrNone, EitherOr,\
isCallable
from reportlab.lib.attrmap import *
from reportlab.pdfgen.canvas import Canvas
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, Ellipse, \
Wedge, String, STATE_DEFAULTS
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
from reportlab.graphics.charts.areas import PlotArea
from reportlab.graphics.charts.legends import _objStr
from reportlab.graphics.charts.piecharts import WedgeLabel
from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol
class StrandProperty(PropHolder):
_attrMap = AttrMap(
strokeWidth = AttrMapValue(isNumber,desc='width'),
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (3,2)'),
symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.',advancedUsage=1),
symbolSize= AttrMapValue(isNumber, desc='Symbol size.',advancedUsage=1),
name = AttrMapValue(isStringOrNone, desc='Name of the strand.'),
)
def __init__(self):
self.strokeWidth = 1
self.fillColor = None
self.strokeColor = STATE_DEFAULTS["strokeColor"]
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
self.symbol = None
self.symbolSize = 5
self.name = None
class SpokeProperty(PropHolder):
_attrMap = AttrMap(
strokeWidth = AttrMapValue(isNumber,desc='width'),
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (2,1)'),
labelRadius = AttrMapValue(isNumber,desc='label radius',advancedUsage=1),
visible = AttrMapValue(isBoolean,desc="True if the spoke line is to be drawn"),
)
def __init__(self,**kw):
self.strokeWidth = 0.5
self.fillColor = None
self.strokeColor = STATE_DEFAULTS["strokeColor"]
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
self.visible = 1
self.labelRadius = 1.05
class SpokeLabel(WedgeLabel):
def __init__(self,**kw):
WedgeLabel.__init__(self,**kw)
if '_text' not in list(kw.keys()): self._text = ''
class StrandLabel(SpokeLabel):
_attrMap = AttrMap(BASE=SpokeLabel,
format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),desc="Format for the label"),
dR = AttrMapValue(isNumberOrNone,desc="radial shift for label"),
)
def __init__(self,**kw):
self.format = ''
self.dR = 0
SpokeLabel.__init__(self,**kw)
def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty):
L = labelClass()
L._text = text
L.x = cx + radius*car
L.y = cy + radius*sar
L._pmv = angle*180/pi
L.boxAnchor = sty.boxAnchor
L.dx = sty.dx
L.dy = sty.dy
L.angle = sty.angle
L.boxAnchor = sty.boxAnchor
L.boxStrokeColor = sty.boxStrokeColor
L.boxStrokeWidth = sty.boxStrokeWidth
L.boxFillColor = sty.boxFillColor
L.strokeColor = sty.strokeColor
L.strokeWidth = sty.strokeWidth
L.leading = sty.leading
L.width = sty.width
L.maxWidth = sty.maxWidth
L.height = sty.height
L.textAnchor = sty.textAnchor
L.visible = sty.visible
L.topPadding = sty.topPadding
L.leftPadding = sty.leftPadding
L.rightPadding = sty.rightPadding
L.bottomPadding = sty.bottomPadding
L.fontName = sty.fontName
L.fontSize = sty.fontSize
L.fillColor = sty.fillColor
return L
class SpiderChart(PlotArea):
_attrMap = AttrMap(BASE=PlotArea,
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
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'"),
strands = AttrMapValue(None, desc="collection of strand descriptor objects"),
spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"),
strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"),
spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"),
)
def makeSwatchSample(self, rowNo, x, y, width, height):
baseStyle = self.strands
styleIdx = rowNo % len(baseStyle)
style = baseStyle[styleIdx]
strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None))
fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None))
strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0))
symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None))
ym = y+height/2.0
if fillColor is None and strokeColor is not None and strokeWidth>0:
bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor,
strokeDashArray=strokeDashArray)
elif fillColor is not None:
bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor,
strokeDashArray=strokeDashArray,fillColor=fillColor)
else:
bg = None
if symbol:
symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color)
if bg:
g = Group()
g.add(bg)
g.add(symbol)
return g
return symbol or bg
def getSeriesName(self,i,default=None):
'''return series name i or default'''
return _objStr(getattr(self.strands[i],'name',default))
def __init__(self):
PlotArea.__init__(self)
self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]]
self.labels = None # or list of strings
self.labels = ['a','b','c','d','e','f']
self.startAngle = 90
self.direction = "clockwise"
self.strands = TypedPropertyCollection(StrandProperty)
self.spokes = TypedPropertyCollection(SpokeProperty)
self.spokeLabels = TypedPropertyCollection(SpokeLabel)
self.spokeLabels._text = None
self.strandLabels = TypedPropertyCollection(StrandLabel)
self.x = 10
self.y = 10
self.width = 180
self.height = 180
def demo(self):
d = Drawing(200, 200)
d.add(SpiderChart())
return d
def normalizeData(self, outer = 0.0):
"""Turns data into normalized ones where each datum is < 1.0,
and 1.0 = maximum radius. Adds 10% at outside edge by default"""
data = self.data
assert min(list(map(min,data))) >=0, "Cannot do spider plots of negative numbers!"
norm = max(list(map(max,data)))
norm *= (1.0+outer)
if norm<1e-9: norm = 1.0
self._norm = norm
return [[e/norm for e in row] for row in data]
def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=StrandLabel):
"Draw a label for a given item in the list."
fmt = sty.format
value = radius*self._norm
if not fmt:
text = None
elif isinstance(fmt,str):
if fmt == 'values':
text = sty._text
else:
text = fmt % value
elif hasattr(fmt,'__call__'):
text = fmt(value)
else:
raise ValueError("Unknown formatter type %s, expected string or function" % fmt)
if text:
dR = sty.dR
if dR:
radius += dR/self._radius
L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty)
if dR<0: L._anti = 1
else:
L = None
return L
def draw(self):
# normalize slice data
g = self.makeBackground() or Group()
xradius = self.width/2.0
yradius = self.height/2.0
self._radius = radius = min(xradius, yradius)
cx = self.x + xradius
cy = self.y + yradius
data = self.normalizeData()
self._seriesCount = len(data)
n = len(data[0])
#labels
if self.labels is None:
labels = [''] * n
else:
labels = self.labels
#there's no point in raising errors for less than enough errors if
#we silently create all for the extreme case of no labels.
i = n-len(labels)
if i>0:
labels = labels + ['']*i
S = []
STRANDS = []
STRANDAREAS = []
syms = []
labs = []
csa = []
angle = self.startAngle*pi/180
direction = self.direction == "clockwise" and -1 or 1
angleBetween = direction*(2 * pi)/float(n)
spokes = self.spokes
spokeLabels = self.spokeLabels
for i in range(n):
car = cos(angle)*radius
sar = sin(angle)*radius
csa.append((car,sar,angle))
si = self.spokes[i]
if si.visible:
spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray)
S.append(spoke)
sli = spokeLabels[i]
text = sli._text
if not text: text = labels[i]
if text:
S.append(_setupLabel(WedgeLabel, text, si.labelRadius, cx, cy, angle, car, sar, sli))
angle += angleBetween
# now plot the polygons
rowIdx = 0
strands = self.strands
strandLabels = self.strandLabels
for row in data:
# series plot
rsty = strands[rowIdx]
points = []
car, sar = csa[-1][:2]
r = row[-1]
points.append(cx+car*r)
points.append(cy+sar*r)
for i in range(n):
car, sar, angle = csa[i]
r = row[i]
points.append(cx+car*r)
points.append(cy+sar*r)
L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=StrandLabel)
if L: labs.append(L)
sty = strands[(rowIdx,i)]
uSymbol = sty.symbol
# put in a marker, if it needs one
if uSymbol:
s_x = cx+car*r
s_y = cy+sar*r
s_fillColor = sty.fillColor
s_strokeColor = sty.strokeColor
s_strokeWidth = sty.strokeWidth
s_angle = 0
s_size = sty.symbolSize
if type(uSymbol) is type(''):
symbol = makeMarker(uSymbol,
size = s_size,
x = s_x,
y = s_y,
fillColor = s_fillColor,
strokeColor = s_strokeColor,
strokeWidth = s_strokeWidth,
angle = s_angle,
)
else:
symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor)
for k,v in (('size', s_size), ('fillColor', s_fillColor),
('x', s_x), ('y', s_y),
('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth),
('angle',s_angle),):
if getattr(symbol,k,None) is None:
try:
setattr(symbol,k,v)
except:
pass
syms.append(symbol)
# make up the 'strand'
if rsty.fillColor:
strand = Polygon(points)
strand.fillColor = rsty.fillColor
strand.strokeColor = None
strand.strokeWidth = 0
STRANDAREAS.append(strand)
if rsty.strokeColor and rsty.strokeWidth:
strand = PolyLine(points)
strand.strokeColor = rsty.strokeColor
strand.strokeWidth = rsty.strokeWidth
strand.strokeDashArray = rsty.strokeDashArray
STRANDS.append(strand)
rowIdx += 1
for s in (STRANDAREAS+STRANDS+syms+S+labs): g.add(s)
return g
def sample1():
"Make a simple spider chart"
d = Drawing(400, 400)
sp = SpiderChart()
sp.x = 50
sp.y = 50
sp.width = 300
sp.height = 300
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
sp.labels = ['a','b','c','d','e','f']
sp.strands[0].strokeColor = colors.cornsilk
sp.strands[1].strokeColor = colors.cyan
sp.strands[2].strokeColor = colors.palegreen
sp.strands[0].fillColor = colors.cornsilk
sp.strands[1].fillColor = colors.cyan
sp.strands[2].fillColor = colors.palegreen
sp.spokes.strokeDashArray = (2,2)
d.add(sp)
return d
def sample2():
"Make a spider chart with markers, but no fill"
d = Drawing(400, 400)
sp = SpiderChart()
sp.x = 50
sp.y = 50
sp.width = 300
sp.height = 300
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
sp.labels = ['U','V','W','X','Y','Z']
sp.strands.strokeWidth = 1
sp.strands[0].fillColor = colors.pink
sp.strands[1].fillColor = colors.lightblue
sp.strands[2].fillColor = colors.palegreen
sp.strands[0].strokeColor = colors.red
sp.strands[1].strokeColor = colors.blue
sp.strands[2].strokeColor = colors.green
sp.strands.symbol = "FilledDiamond"
sp.strands[1].symbol = makeMarker("Circle")
sp.strands[1].symbol.strokeWidth = 0.5
sp.strands[1].symbol.fillColor = colors.yellow
sp.strands.symbolSize = 6
sp.strandLabels[0,3]._text = 'special'
sp.strandLabels[0,1]._text = 'one'
sp.strandLabels[0,0]._text = 'zero'
sp.strandLabels[1,0]._text = 'Earth'
sp.strandLabels[2,2]._text = 'Mars'
sp.strandLabels.format = 'values'
sp.strandLabels.dR = -5
d.add(sp)
return d
if __name__=='__main__':
d = sample1()
from reportlab.graphics.renderPDF import drawToFile
drawToFile(d, 'spider.pdf')
d = sample2()
drawToFile(d, 'spider2.pdf')

View File

@@ -0,0 +1,466 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py
__version__=''' $Id$ '''
import string
from reportlab.lib import colors
from reportlab.lib.utils import simpleSplit, _simpleSplit
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString, isNoneOrCallable
from reportlab.lib.attrmap import *
from reportlab.pdfbase.pdfmetrics import stringWidth, getAscentDescent
from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS
from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath
from reportlab.graphics.widgetbase import Widget, PropHolder
from reportlab.graphics.shapes import _baseGFontName
_gs = None
_A2BA= {
'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'},
'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'},
}
def _pathNumTrunc(n):
if int(n)==n: return int(n)
return round(n,5)
def _processGlyph(G, truncate=1, pathReverse=0):
O = []
P = []
R = []
if G and len(G)==1 and G[0][0]=='lineTo':
G = (('moveToClosed',)+G[0][1:],)+G #hack fix for some errors
for g in G+(('end',),):
op = g[0]
if O and op in ['moveTo', 'moveToClosed','end']:
if O[0]=='moveToClosed':
O = O[1:]
if pathReverse:
for i in range(0,len(P),2):
P[i+1], P[i] = P[i:i+2]
P.reverse()
O.reverse()
O.insert(0,'moveTo')
O.append('closePath')
i = 0
if truncate: P = list(map(_pathNumTrunc,P))
for o in O:
j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)]
if o=='closePath':
R.append(o)
else:
R.append((o,)+ tuple(P[i:j]))
i = j
O = []
P = []
O.append(op)
P.extend(g[1:])
return R
def _text2PathDescription(text, x=0, y=0, fontName=_baseGFontName, fontSize=1000,
anchor='start', truncate=1, pathReverse=0):
from reportlab.graphics import renderPM, _renderPM
_gs = _renderPM.gstate(1,1)
renderPM._setFont(_gs,fontName,fontSize)
P = []
if not anchor=='start':
textLen = stringWidth(text, fontName,fontSize)
if anchor=='end':
x = x-textLen
elif anchor=='middle':
x = x - textLen/2.
for g in _gs._stringPath(text,x,y):
P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse))
return P
def _text2Path(text, x=0, y=0, fontName=_baseGFontName, fontSize=1000,
anchor='start', truncate=1, pathReverse=0,**kwds):
return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName,
fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse),**kwds)
_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'}
class Label(Widget):
"""A text label to attach to something else, such as a chart axis.
This allows you to specify an offset, angle and many anchor
properties relative to the label's origin. It allows, for example,
angled multiline axis labels.
"""
# fairly straight port of Robin Becker's textbox.py to new widgets
# framework.
_attrMap = AttrMap(
x = AttrMapValue(isNumber,desc=''),
y = AttrMapValue(isNumber,desc=''),
dx = AttrMapValue(isNumber,desc='delta x - offset'),
dy = AttrMapValue(isNumber,desc='delta y - offset'),
angle = AttrMapValue(isNumber,desc='angle of label: default (0), 90 is vertical, 180 is upside down, etc'),
boxAnchor = AttrMapValue(isBoxAnchor,desc='anchoring point of the label'),
boxStrokeColor = AttrMapValue(isColorOrNone,desc='border color of the box'),
boxStrokeWidth = AttrMapValue(isNumber,desc='border width'),
boxFillColor = AttrMapValue(isColorOrNone,desc='the filling color of the box'),
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi'),desc="one of ('normal','anti','lo','hi')"),
fillColor = AttrMapValue(isColorOrNone,desc='label text color'),
strokeColor = AttrMapValue(isColorOrNone,desc='label text border color'),
strokeWidth = AttrMapValue(isNumber,desc='label text border width'),
text = AttrMapValue(isString,desc='the actual text to display'),
fontName = AttrMapValue(isString,desc='the name of the font used'),
fontSize = AttrMapValue(isNumber,desc='the size of the font'),
leading = AttrMapValue(isNumberOrNone,desc=''),
width = AttrMapValue(isNumberOrNone,desc='the width of the label'),
maxWidth = AttrMapValue(isNumberOrNone,desc='maximum width the label can grow to'),
height = AttrMapValue(isNumberOrNone,desc='the height of the text'),
textAnchor = AttrMapValue(isTextAnchor,desc='the anchoring point of the text inside the label'),
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
topPadding = AttrMapValue(isNumber,desc='padding at top of box'),
leftPadding = AttrMapValue(isNumber,desc='padding at left of box'),
rightPadding = AttrMapValue(isNumber,desc='padding at right of box'),
bottomPadding = AttrMapValue(isNumber,desc='padding at bottom of box'),
useAscentDescent = AttrMapValue(isBoolean,desc="If True then the font's Ascent & Descent will be used to compute default heights and baseline."),
customDrawChanger = AttrMapValue(isNoneOrCallable,desc="An instance of CustomDrawChanger to modify the behavior at draw time", _advancedUsage=1),
)
def __init__(self,**kw):
self._setKeywords(**kw)
self._setKeywords(
_text = 'Multi-Line\nString',
boxAnchor = 'c',
angle = 0,
x = 0,
y = 0,
dx = 0,
dy = 0,
topPadding = 0,
leftPadding = 0,
rightPadding = 0,
bottomPadding = 0,
boxStrokeWidth = 0.5,
boxStrokeColor = None,
boxTarget = 'normal',
strokeColor = None,
boxFillColor = None,
leading = None,
width = None,
maxWidth = None,
height = None,
fillColor = STATE_DEFAULTS['fillColor'],
fontName = STATE_DEFAULTS['fontName'],
fontSize = STATE_DEFAULTS['fontSize'],
strokeWidth = 0.1,
textAnchor = 'start',
visible = 1,
useAscentDescent = False,
)
def setText(self, text):
"""Set the text property. May contain embedded newline characters.
Called by the containing chart or axis."""
self._text = text
def setOrigin(self, x, y):
"""Set the origin. This would be the tick mark or bar top relative to
which it is defined. Called by the containing chart or axis."""
self.x = x
self.y = y
def demo(self):
"""This shows a label positioned with its top right corner
at the top centre of the drawing, and rotated 45 degrees."""
d = Drawing(200, 100)
# mark the origin of the label
d.add(Circle(100,90, 5, fillColor=colors.green))
lab = Label()
lab.setOrigin(100,90)
lab.boxAnchor = 'ne'
lab.angle = 45
lab.dx = 0
lab.dy = -20
lab.boxStrokeColor = colors.green
lab.setText('Another\nMulti-Line\nString')
d.add(lab)
return d
def _getBoxAnchor(self):
'''hook for allowing special box anchor effects'''
ba = self.boxAnchor
if ba in ('autox', 'autoy'):
angle = self.angle
na = (int((angle%360)/45.)*45)%360
if not (na % 90): # we have a right angle case
da = (angle - na) % 360
if abs(da)>5:
na = na + (da>0 and 45 or -45)
ba = _A2BA[ba[-1]][na]
return ba
def computeSize(self):
# the thing will draw in its own coordinate system
self._lineWidths = []
topPadding = self.topPadding
leftPadding = self.leftPadding
rightPadding = self.rightPadding
bottomPadding = self.bottomPadding
self._lines = simpleSplit(self._text,self.fontName,self.fontSize,self.maxWidth)
if not self.width:
self._width = leftPadding+rightPadding
if self._lines:
self._lineWidths = [stringWidth(line,self.fontName,self.fontSize) for line in self._lines]
self._width += max(self._lineWidths)
else:
self._width = self.width
if self.useAscentDescent:
self._ascent, self._descent = getAscentDescent(self.fontName,self.fontSize)
self._baselineRatio = self._ascent/(self._ascent-self._descent)
else:
self._baselineRatio = 1/1.2
if self.leading:
self._leading = self.leading
elif self.useAscentDescent:
self._leading = self._ascent - self._descent
else:
self._leading = self.fontSize*1.2
self._height = self.height or (self._leading*len(self._lines) + topPadding + bottomPadding)
self._ewidth = (self._width-leftPadding-rightPadding)
self._eheight = (self._height-topPadding-bottomPadding)
boxAnchor = self._getBoxAnchor()
if boxAnchor in ['n','ne','nw']:
self._top = -topPadding
elif boxAnchor in ['s','sw','se']:
self._top = self._height-topPadding
else:
self._top = 0.5*self._eheight
self._bottom = self._top - self._eheight
if boxAnchor in ['ne','e','se']:
self._left = leftPadding - self._width
elif boxAnchor in ['nw','w','sw']:
self._left = leftPadding
else:
self._left = -self._ewidth*0.5
self._right = self._left+self._ewidth
def _getTextAnchor(self):
'''This can be overridden to allow special effects'''
ta = self.textAnchor
if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()]
return ta
def _rawDraw(self):
_text = self._text
self._text = _text or ''
self.computeSize()
self._text = _text
g = Group()
g.translate(self.x + self.dx, self.y + self.dy)
g.rotate(self.angle)
y = self._top - self._leading*self._baselineRatio
textAnchor = self._getTextAnchor()
if textAnchor == 'start':
x = self._left
elif textAnchor == 'middle':
x = self._left + self._ewidth*0.5
else:
x = self._right
# paint box behind text just in case they
# fill it
if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
g.add(Rect( self._left-self.leftPadding,
self._bottom-self.bottomPadding,
self._width,
self._height,
strokeColor=self.boxStrokeColor,
strokeWidth=self.boxStrokeWidth,
fillColor=self.boxFillColor)
)
fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize
strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, self._leading
svgAttrs=getattr(self,'_svgAttrs',{})
if strokeColor:
for line in self._lines:
s = _text2Path(line, x, y, fontName, fontSize, textAnchor)
s.fillColor = fillColor
s.strokeColor = strokeColor
s.strokeWidth = strokeWidth
g.add(s)
y -= leading
else:
for line in self._lines:
s = String(x, y, line, _svgAttrs=svgAttrs)
s.textAnchor = textAnchor
s.fontName = fontName
s.fontSize = fontSize
s.fillColor = fillColor
g.add(s)
y -= leading
return g
def draw(self):
customDrawChanger = getattr(self,'customDrawChanger',None)
if customDrawChanger:
customDrawChanger(True,self)
try:
return self._rawDraw()
finally:
customDrawChanger(False,self)
else:
return self._rawDraw()
class LabelDecorator:
_attrMap = AttrMap(
x = AttrMapValue(isNumberOrNone,desc=''),
y = AttrMapValue(isNumberOrNone,desc=''),
dx = AttrMapValue(isNumberOrNone,desc=''),
dy = AttrMapValue(isNumberOrNone,desc=''),
angle = AttrMapValue(isNumberOrNone,desc=''),
boxAnchor = AttrMapValue(isBoxAnchor,desc=''),
boxStrokeColor = AttrMapValue(isColorOrNone,desc=''),
boxStrokeWidth = AttrMapValue(isNumberOrNone,desc=''),
boxFillColor = AttrMapValue(isColorOrNone,desc=''),
fillColor = AttrMapValue(isColorOrNone,desc=''),
strokeColor = AttrMapValue(isColorOrNone,desc=''),
strokeWidth = AttrMapValue(isNumberOrNone),desc='',
fontName = AttrMapValue(isNoneOrString,desc=''),
fontSize = AttrMapValue(isNumberOrNone,desc=''),
leading = AttrMapValue(isNumberOrNone,desc=''),
width = AttrMapValue(isNumberOrNone,desc=''),
maxWidth = AttrMapValue(isNumberOrNone,desc=''),
height = AttrMapValue(isNumberOrNone,desc=''),
textAnchor = AttrMapValue(isTextAnchor,desc=''),
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
)
def __init__(self):
self.textAnchor = 'start'
self.boxAnchor = 'w'
for a in self._attrMap.keys():
if not hasattr(self,a): setattr(self,a,None)
def decorate(self,l,L):
chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo
L.setText(chart.categoryAxis.categoryNames[colNo])
g.add(L)
def __call__(self,l):
from copy import deepcopy
L = Label()
for a,v in self.__dict__.items():
if v is None: v = getattr(l,a,None)
setattr(L,a,v)
self.decorate(l,L)
isOffsetMode=OneOf('high','low','bar','axis')
class LabelOffset(PropHolder):
_attrMap = AttrMap(
posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"),
pos = AttrMapValue(isNumber,desc='Value for positive elements'),
negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"),
neg = AttrMapValue(isNumber,desc='Value for negative elements'),
)
def __init__(self):
self.posMode=self.negMode='axis'
self.pos = self.neg = 0
def _getValue(self, chart, val):
flipXY = chart._flipXY
A = chart.categoryAxis
jA = A.joinAxis
if val>=0:
mode = self.posMode
delta = self.pos
else:
mode = self.negMode
delta = self.neg
if flipXY:
v = A._x
else:
v = A._y
if jA:
if flipXY:
_v = jA._x
else:
_v = jA._y
if mode=='high':
v = _v + jA._length
elif mode=='low':
v = _v
elif mode=='bar':
v = _v+val
return v+delta
NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset))
class PMVLabel(Label):
_attrMap = AttrMap(
BASE=Label,
)
def __init__(self):
Label.__init__(self)
self._pmv = 0
def _getBoxAnchor(self):
a = Label._getBoxAnchor(self)
if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a]
return a
def _getTextAnchor(self):
a = Label._getTextAnchor(self)
if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
return a
class BarChartLabel(PMVLabel):
"""
An extended Label allowing for nudging, lines visibility etc
"""
_attrMap = AttrMap(
BASE=PMVLabel,
lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"),
lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"),
fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"),
fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"),
nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"),
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi','mid'),desc="one of ('normal','anti','lo','hi','mid')"),
)
def __init__(self):
PMVLabel.__init__(self)
self.lineStrokeWidth = 0
self.lineStrokeColor = None
self.fixedStart = self.fixedEnd = None
self.nudge = 0
class NA_Label(BarChartLabel):
"""
An extended Label allowing for nudging, lines visibility etc
"""
_attrMap = AttrMap(
BASE=BarChartLabel,
text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"),
)
def __init__(self):
BarChartLabel.__init__(self)
self.text = 'n/a'
NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label))
from reportlab.graphics.charts.utils import CustomDrawChanger
class RedNegativeChanger(CustomDrawChanger):
def __init__(self,fillColor=colors.red):
CustomDrawChanger.__init__(self)
self.fillColor = fillColor
def _changer(self,obj):
R = {}
if obj._text.startswith('-'):
R['fillColor'] = obj.fillColor
obj.fillColor = self.fillColor
return R

View File

@@ -0,0 +1,390 @@
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/utils.py
__version__=''' $Id$ '''
__doc__="Utilities used here and there."
from time import mktime, gmtime, strftime
from math import log10, pi, floor, sin, cos, sqrt, hypot
import weakref
from reportlab.graphics.shapes import transformPoint, transformPoints, inverse, Ellipse, Group, String, Path, numericXShift
from reportlab.lib.utils import flatten
from reportlab.pdfbase.pdfmetrics import stringWidth
### Dinu's stuff used in some line plots (likely to vansih).
def mkTimeTuple(timeString):
"Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module."
list = [0] * 9
dd, mm, yyyy = list(map(int, timeString.split('/')))
list[:3] = [yyyy, mm, dd]
return tuple(list)
def str2seconds(timeString):
"Convert a number of seconds since the epoch into a date string."
return mktime(mkTimeTuple(timeString))
def seconds2str(seconds):
"Convert a date string into the number of seconds since the epoch."
return strftime('%Y-%m-%d', gmtime(seconds))
### Aaron's rounding function for making nice values on axes.
def nextRoundNumber(x):
"""Return the first 'nice round number' greater than or equal to x
Used in selecting apropriate tick mark intervals; we say we want
an interval which places ticks at least 10 points apart, work out
what that is in chart space, and ask for the nextRoundNumber().
Tries the series 1,2,5,10,20,50,100.., going up or down as needed.
"""
#guess to nearest order of magnitude
if x in (0, 1):
return x
if x < 0:
return -1.0 * nextRoundNumber(-x)
else:
lg = int(log10(x))
if lg == 0:
if x < 1:
base = 0.1
else:
base = 1.0
elif lg < 0:
base = 10.0 ** (lg - 1)
else:
base = 10.0 ** lg # e.g. base(153) = 100
# base will always be lower than x
if base >= x:
return base * 1.0
elif (base * 2) >= x:
return base * 2.0
elif (base * 5) >= x:
return base * 5.0
else:
return base * 10.0
_intervals=(.1, .2, .25, .5)
_j_max=len(_intervals)-1
def find_interval(lo,hi,I=5):
'determine tick parameters for range [lo, hi] using I intervals'
if lo >= hi:
if lo==hi:
if lo==0:
lo = -.1
hi = .1
else:
lo = 0.9*lo
hi = 1.1*hi
else:
raise ValueError("lo>hi")
x=(hi - lo)/float(I)
b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1
b = b
while 1:
a = x/b
if a<=_intervals[-1]: break
b = b*10
j = 0
while a>_intervals[j]: j = j + 1
while 1:
ss = _intervals[j]*b
n = lo/ss
l = int(n)-(n<0)
n = ss*l
x = ss*(l+I)
a = I*ss
if n>0:
if a>=hi:
n = 0.0
x = a
elif hi<0:
a = -a
if lo>a:
n = a
x = 0
if hi<=x and n<=lo: break
j = j + 1
if j>_j_max:
j = 0
b = b*10
return n, x, ss, lo - n + x - hi
def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None):
if grid:
t = divmod(lower,grid)[0] * grid
hi, z = divmod(upper,grid)
if z>1e-8: hi = hi+1
hi = hi*grid
else:
try:
n[0]
except TypeError:
n = range(max(1,n-2),max(n+3,2))
w = 1e308
for i in n:
z=find_interval(lower,upper,i)
if z[3]<w:
t, hi, grid = z[:3]
w=z[3]
return t, hi, grid
def ticks(lower, upper, n=(4,5,6,7,8,9), split=1, percent=0, grid=None, labelVOffset=0):
'''
return tick positions and labels for range lower<=x<=upper
n=number of intervals to try (can be a list or sequence)
split=1 return ticks then labels else (tick,label) pairs
'''
t, hi, grid = find_good_grid(lower, upper, n, grid)
power = floor(log10(grid))
if power==0: power = 1
w = grid/10.**power
w = int(w)!=w
if power > 3 or power < -3:
format = '%+'+repr(w+7)+'.0e'
else:
if power >= 0:
digits = int(power)+w
format = '%' + repr(digits)+'.0f'
else:
digits = w-int(power)
format = '%'+repr(digits+2)+'.'+repr(digits)+'f'
if percent: format=format+'%%'
T = []
n = int(float(hi-t)/grid+0.1)+1
if split:
labels = []
for i in range(n):
v = t+grid*i
T.append(v)
labels.append(format % (v+labelVOffset))
return T, labels
else:
for i in range(n):
v = t+grid*i
T.append((v, format % (v+labelVOffset)))
return T
def findNones(data):
m = len(data)
if None in data:
b = 0
while b<m and data[b] is None:
b += 1
if b==m: return data
l = m-1
while data[l] is None:
l -= 1
l+=1
if b or l: data = data[b:l]
I = [i for i in range(len(data)) if data[i] is None]
for i in I:
data[i] = 0.5*(data[i-1]+data[i+1])
return b, l, data
return 0,m,data
def pairFixNones(pairs):
Y = [x[1] for x in pairs]
b,l,nY = findNones(Y)
m = len(Y)
if b or l<m or nY!=Y:
if b or l<m: pairs = pairs[b:l]
pairs = [(x[0],y) for x,y in zip(pairs,nY)]
return pairs
def maverage(data,n=6):
data = (n-1)*[data[0]]+data
data = [float(sum(data[i-n:i]))/n for i in range(n,len(data)+1)]
return data
def pairMaverage(data,n=6):
return [(x[0],s) for x,s in zip(data, maverage([x[1] for x in data],n))]
class DrawTimeCollector(object):
'''
generic mechanism for collecting information about nodes at the time they are about to be drawn
'''
def __init__(self,formats=['gif']):
self._nodes = weakref.WeakKeyDictionary()
self.clear()
self._pmcanv = None
self.formats = formats
self.disabled = False
def clear(self):
self._info = []
self._info_append = self._info.append
def record(self,func,node,*args,**kwds):
self._nodes[node] = (func,args,kwds)
node.__dict__['_drawTimeCallback'] = self
def __call__(self,node,canvas,renderer):
func = self._nodes.get(node,None)
if func:
func, args, kwds = func
i = func(node,canvas,renderer, *args, **kwds)
if i is not None: self._info_append(i)
@staticmethod
def rectDrawTimeCallback(node,canvas,renderer,**kwds):
A = getattr(canvas,'ctm',None)
if not A: return
x1 = node.x
y1 = node.y
x2 = x1 + node.width
y2 = y1 + node.height
D = kwds.copy()
D['rect']=DrawTimeCollector.transformAndFlatten(A,((x1,y1),(x2,y2)))
return D
@staticmethod
def transformAndFlatten(A,p):
''' transform an flatten a list of points
A transformation matrix
p points [(x0,y0),....(xk,yk).....]
'''
if tuple(A)!=(1,0,0,1,0,0):
iA = inverse(A)
p = transformPoints(iA,p)
return tuple(flatten(p))
@property
def pmcanv(self):
if not self._pmcanv:
import renderPM
self._pmcanv = renderPM.PMCanvas(1,1)
return self._pmcanv
def wedgeDrawTimeCallback(self,node,canvas,renderer,**kwds):
A = getattr(canvas,'ctm',None)
if not A: return
if isinstance(node,Ellipse):
c = self.pmcanv
c.ellipse(node.cx, node.cy, node.rx,node.ry)
p = c.vpath
p = [(x[1],x[2]) for x in p]
else:
p = node.asPolygon().points
p = [(p[i],p[i+1]) for i in range(0,len(p),2)]
D = kwds.copy()
D['poly'] = self.transformAndFlatten(A,p)
return D
def save(self,fnroot):
'''
save the current information known to this collector
fnroot is the root name of a resource to name the saved info
override this to get the right semantics for your collector
'''
import pprint
f=open(fnroot+'.default-collector.out','w')
try:
pprint.pprint(self._info,f)
finally:
f.close()
def xyDist(xxx_todo_changeme, xxx_todo_changeme1 ):
'''return distance between two points'''
(x0,y0) = xxx_todo_changeme
(x1,y1) = xxx_todo_changeme1
return hypot((x1-x0),(y1-y0))
def lineSegmentIntersect(xxx_todo_changeme2, xxx_todo_changeme3, xxx_todo_changeme4, xxx_todo_changeme5
):
(x00,y00) = xxx_todo_changeme2
(x01,y01) = xxx_todo_changeme3
(x10,y10) = xxx_todo_changeme4
(x11,y11) = xxx_todo_changeme5
p = x00,y00
r = x01-x00,y01-y00
q = x10,y10
s = x11-x10,y11-y10
rs = float(r[0]*s[1]-r[1]*s[0])
qp = q[0]-p[0],q[1]-p[1]
qpr = qp[0]*r[1]-qp[1]*r[0]
qps = qp[0]*s[1]-qp[1]*s[0]
if abs(rs)<1e-8:
if abs(qpr)<1e-8: return 'collinear'
return None
t = qps/rs
u = qpr/rs
if 0<=t<=1 and 0<=u<=1:
return p[0]+t*r[0], p[1]+t*r[1]
def makeCircularString(x, y, radius, angle, text, fontName, fontSize, inside=0, G=None,textAnchor='start'):
'''make a group with circular text in it'''
if not G: G = Group()
angle %= 360
pi180 = pi/180
phi = angle*pi180
width = stringWidth(text, fontName, fontSize)
sig = inside and -1 or 1
hsig = sig*0.5
sig90 = sig*90
if textAnchor!='start':
if textAnchor=='middle':
phi += sig*(0.5*width)/radius
elif textAnchor=='end':
phi += sig*float(width)/radius
elif textAnchor=='numeric':
phi += sig*float(numericXShift(textAnchor,text,width,fontName,fontSize,None))/radius
for letter in text:
width = stringWidth(letter, fontName, fontSize)
beta = float(width)/radius
h = Group()
h.add(String(0, 0, letter, fontName=fontName,fontSize=fontSize,textAnchor="start"))
h.translate(x+cos(phi)*radius,y+sin(phi)*radius) #translate to radius and angle
h.rotate((phi-hsig*beta)/pi180-sig90) # rotate as needed
G.add(h) #add to main group
phi -= sig*beta #increment
return G
class CustomDrawChanger:
'''
a class to simplify making changes at draw time
'''
def __init__(self):
self.store = None
def __call__(self,change,obj):
if change:
self.store = self._changer(obj)
assert isinstance(self.store,dict), '%s.changer should return a dict of changed attributes' % self.__class__.__name__
elif self.store is not None:
for a,v in self.store.items():
setattr(obj,a,v)
self.store = None
def _changer(self,obj):
'''
When implemented this method should return a dictionary of
original attribute values so that a future self(False,obj)
can restore them.
'''
raise RuntimeError('Abstract method _changer called')

View File

@@ -0,0 +1,233 @@
from reportlab.lib import colors
from reportlab.lib.attrmap import *
from reportlab.pdfgen.canvas import Canvas
from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, Polygon, Line
def _getShaded(col,shd=None,shading=0.1):
if shd is None:
from reportlab.lib.colors import Blacker
if col: shd = Blacker(col,1-shading)
return shd
def _getLit(col,shd=None,lighting=0.1):
if shd is None:
from reportlab.lib.colors import Whiter
if col: shd = Whiter(col,1-lighting)
return shd
def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth,
fillColor=None, fillColorShaded=None,
strokeColor=None, strokeWidth=1, shading=0.1):
fillColorShaded = _getShaded(fillColor,None,shading)
fillColorShadedTop = _getShaded(fillColor,None,shading/2.0)
def _add_3d_bar(x1, x2, y1, y2, xoff, yoff,
G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor):
G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2),
strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1))
usd = max(y0, yhigh)
if xdepth or ydepth:
if y0!=yhigh: #non-zero height
_add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side
_add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top
G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh),
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front
if xdepth or ydepth:
G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded))
class _YStrip:
def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1):
self.y0 = y0
self.y1 = y1
self.slope = slope
self.fillColor = fillColor
self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading)
def _ystrip_poly( x0, x1, y0, y1, xoff, yoff):
return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1]
def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1,
theta_x, theta_y,
fillColor, fillColorShaded=None, tileWidth=1,
strokeColor=None, strokeWidth=None, strokeDashArray=None,
shading=0.1):
zwidth = abs(z1-z0)
xdepth = zwidth*theta_x
ydepth = zwidth*theta_y
depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth)
x = float(x1-x0)
slope = x==0 and 1e150 or (y1-y0)/x
c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor
zy0 = z0*theta_y
zx0 = z0*theta_x
tileStrokeWidth = 0.6
if tileWidth is None:
D = [(x1,y1)]
else:
T = ((y1-y0)**2+(x1-x0)**2)**0.5
tileStrokeWidth *= tileWidth
if T<tileWidth:
D = [(x1,y1)]
else:
n = int(T/float(tileWidth))+1
dx = float(x1-x0)/n
dy = float(y1-y0)/n
D = []
a = D.append
for i in range(1,n):
a((x0+dx*i,y0+dy*i))
a = G.add
x_0 = x0+zx0
y_0 = y0+zy0
for x,y in D:
x_1 = x+zx0
y_1 = y+zy0
P = Polygon(_ystrip_poly(x_0, x_1, y_0, y_1, xdepth, ydepth),
fillColor = c, strokeColor=c, strokeWidth=tileStrokeWidth)
a((0,z0,z1,x_0,y_0,P))
x_0 = x_1
y_0 = y_1
from math import pi, sin, cos
_pi_2 = pi*0.5
_2pi = 2*pi
_180_pi=180./pi
def _2rad(angle):
return angle/_180_pi
def mod_2pi(radians):
radians = radians % _2pi
if radians<-1e-6: radians += _2pi
return radians
def _2deg(o):
return o*_180_pi
def _360(a):
a %= 360
if a<-1e-6: a += 360
return a
_ZERO = 1e-8
_ONE = 1-_ZERO
class _Segment:
def __init__(self,s,i,data):
S = data[s]
x0 = S[i-1][0]
y0 = S[i-1][1]
x1 = S[i][0]
y1 = S[i][1]
if x1<x0:
x0,y0,x1,y1 = x1,y1,x0,y0
# (y-y0)*(x1-x0) = (y1-y0)*(x-x0)
# (x1-x0)*y + (y0-y1)*x = y0*(x1-x0)+x0*(y0-y1)
# a*y+b*x = c
self.a = float(x1-x0)
self.b = float(y1-y0)
self.x0 = x0
self.x1 = x1
self.y0 = y0
self.y1 = y1
self.series = s
self.i = i
self.s = s
def __str__(self):
return '[(%s,%s),(%s,%s)]' % (self.x0,self.y0,self.x1,self.y1)
__repr__ = __str__
def intersect(self,o,I):
'''try to find an intersection with _Segment o
'''
x0 = self.x0
ox0 = o.x0
assert x0<=ox0
if ox0>self.x1: return 1
if o.s==self.s and o.i in (self.i-1,self.i+1): return
a = self.a
b = self.b
oa = o.a
ob = o.b
det = ob*a - oa*b
if -1e-8<det<1e-8: return
dx = x0 - ox0
dy = self.y0 - o.y0
u = (oa*dy - ob*dx)/det
ou = (a*dy - b*dx)/det
if u<0 or u>1 or ou<0 or ou>1: return
x = x0 + u*a
y = self.y0 + u*b
if _ZERO<u<_ONE:
t = self.s,self.i,x,y
if t not in I: I.append(t)
if _ZERO<ou<_ONE:
t = o.s,o.i,x,y
if t not in I: I.append(t)
def _segKey(a):
return (a.x0,a.x1,a.y0,a.y1,a.s,a.i)
def find_intersections(data,small=0):
'''
data is a sequence of series
each series is a list of (x,y) coordinates
where x & y are ints or floats
find_intersections returns a sequence of 4-tuples
i, j, x, y
where i is a data index j is an insertion position for data[i]
and x, y are coordinates of an intersection of series data[i]
with some other series. If correctly implemented we get all such
intersections. We don't count endpoint intersections and consider
parallel lines as non intersecting (even when coincident).
We ignore segments that have an estimated size less than small.
'''
#find all line segments
S = []
a = S.append
for s in range(len(data)):
ds = data[s]
if not ds: continue
n = len(ds)
if n==1: continue
for i in range(1,n):
seg = _Segment(s,i,data)
if seg.a+abs(seg.b)>=small: a(seg)
S.sort(key=_segKey)
I = []
n = len(S)
for i in range(0,n-1):
s = S[i]
for j in range(i+1,n):
if s.intersect(S[j],I)==1: break
I.sort()
return I
if __name__=='__main__':
from reportlab.graphics.shapes import Drawing
from reportlab.lib.colors import lightgrey, pink
D = Drawing(300,200)
_draw_3d_bar(D, 10, 20, 10, 50, 5, 5, fillColor=lightgrey, strokeColor=pink)
_draw_3d_bar(D, 30, 40, 10, 45, 5, 5, fillColor=lightgrey, strokeColor=pink)
D.save(formats=['pdf'],outDir='.',fnRoot='_draw_3d_bar')
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(.2666666667,0.4),(0.1,0.4),(0.1,0.2),(0,0),(1,1)],[(0,1),(0.4,0.1),(1,0.1)]]))
print(find_intersections([[(0.1, 0.2), (0.1, 0.4)], [(0, 1), (0.4, 0.1)]]))
print(find_intersections([[(0.2, 0.4), (0.1, 0.4)], [(0.1, 0.8), (0.4, 0.1)]]))
print(find_intersections([[(0,0),(1,1)],[(0.4,0.1),(1,0.1)]]))
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(0,0),(1,1)],[(0.1,0.8),(0.4,0.1),(1,0.1)]]))