505 lines
17 KiB
Python
505 lines
17 KiB
Python
#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/widgets/grids.py
|
|
__version__=''' $Id$ '''
|
|
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors
|
|
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
|
from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath
|
|
from reportlab.graphics.widgetbase import Widget
|
|
|
|
def frange(start, end=None, inc=None):
|
|
"A range function, that does accept float increments..."
|
|
|
|
if end == None:
|
|
end = start + 0.0
|
|
start = 0.0
|
|
|
|
if inc == None:
|
|
inc = 1.0
|
|
|
|
L = []
|
|
end = end - inc*0.0001 #to avoid numrical problems
|
|
while 1:
|
|
next = start + len(L) * inc
|
|
if inc > 0 and next >= end:
|
|
break
|
|
elif inc < 0 and next <= end:
|
|
break
|
|
L.append(next)
|
|
|
|
return L
|
|
|
|
|
|
def makeDistancesList(list):
|
|
"""Returns a list of distances between adjacent numbers in some input list.
|
|
|
|
E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2]
|
|
"""
|
|
|
|
d = []
|
|
for i in range(len(list[:-1])):
|
|
d.append(list[i+1] - list[i])
|
|
|
|
return d
|
|
|
|
|
|
class Grid(Widget):
|
|
"""This makes a rectangular grid of equidistant stripes.
|
|
|
|
The grid contains an outer border rectangle, and stripes
|
|
inside which can be drawn with lines and/or as solid tiles.
|
|
The drawing order is: outer rectangle, then lines and tiles.
|
|
|
|
The stripes' width is indicated as 'delta'. The sequence of
|
|
stripes can have an offset named 'delta0'. Both values need
|
|
to be positive!
|
|
"""
|
|
|
|
_attrMap = AttrMap(
|
|
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
|
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
|
width = AttrMapValue(isNumber, desc="The grid's width."),
|
|
height = AttrMapValue(isNumber, desc="The grid's height."),
|
|
orientation = AttrMapValue(OneOf(('vertical', 'horizontal')),
|
|
desc='Determines if stripes are vertical or horizontal.'),
|
|
useLines = AttrMapValue(OneOf((0, 1)),
|
|
desc='Determines if stripes are drawn with lines.'),
|
|
useRects = AttrMapValue(OneOf((0, 1)),
|
|
desc='Determines if stripes are drawn with solid rectangles.'),
|
|
delta = AttrMapValue(isNumber,
|
|
desc='Determines the width/height of the stripes.'),
|
|
delta0 = AttrMapValue(isNumber,
|
|
desc='Determines the stripes initial width/height offset.'),
|
|
deltaSteps = AttrMapValue(isListOfNumbers,
|
|
desc='List of deltas to be used cyclically.'),
|
|
stripeColors = AttrMapValue(isListOfColors,
|
|
desc='Colors applied cyclically in the right or upper direction.'),
|
|
fillColor = AttrMapValue(isColorOrNone,
|
|
desc='Background color for entire rectangle.'),
|
|
strokeColor = AttrMapValue(isColorOrNone,
|
|
desc='Color used for lines.'),
|
|
strokeWidth = AttrMapValue(isNumber,
|
|
desc='Width used for lines.'),
|
|
rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'),
|
|
rectStrokeWidth = AttrMapValue(isColorOrNone, desc='Width for outer rect stroke.'),
|
|
)
|
|
|
|
def __init__(self):
|
|
self.x = 0
|
|
self.y = 0
|
|
self.width = 100
|
|
self.height = 100
|
|
self.orientation = 'vertical'
|
|
self.useLines = 0
|
|
self.useRects = 1
|
|
self.delta = 20
|
|
self.delta0 = 0
|
|
self.deltaSteps = []
|
|
self.fillColor = colors.white
|
|
self.stripeColors = [colors.red, colors.green, colors.blue]
|
|
self.strokeColor = colors.black
|
|
self.strokeWidth = 2
|
|
|
|
|
|
def demo(self):
|
|
D = Drawing(100, 100)
|
|
|
|
g = Grid()
|
|
D.add(g)
|
|
|
|
return D
|
|
|
|
def makeOuterRect(self):
|
|
strokeColor = getattr(self,'rectStrokeColor',self.strokeColor)
|
|
strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth)
|
|
if self.fillColor or (strokeColor and strokeWidth):
|
|
rect = Rect(self.x, self.y, self.width, self.height)
|
|
rect.fillColor = self.fillColor
|
|
rect.strokeColor = strokeColor
|
|
rect.strokeWidth = strokeWidth
|
|
return rect
|
|
else:
|
|
return None
|
|
|
|
def makeLinePosList(self, start, isX=0):
|
|
"Returns a list of positions where to place lines."
|
|
|
|
w, h = self.width, self.height
|
|
if isX:
|
|
length = w
|
|
else:
|
|
length = h
|
|
if self.deltaSteps:
|
|
r = [start + self.delta0]
|
|
i = 0
|
|
while 1:
|
|
if r[-1] > start + length:
|
|
del r[-1]
|
|
break
|
|
r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)])
|
|
i = i + 1
|
|
else:
|
|
r = frange(start + self.delta0, start + length, self.delta)
|
|
|
|
r.append(start + length)
|
|
if self.delta0 != 0:
|
|
r.insert(0, start)
|
|
#print 'Grid.makeLinePosList() -> %s' % r
|
|
return r
|
|
|
|
|
|
def makeInnerLines(self):
|
|
# inner grid lines
|
|
group = Group()
|
|
|
|
w, h = self.width, self.height
|
|
|
|
if self.useLines == 1:
|
|
if self.orientation == 'vertical':
|
|
r = self.makeLinePosList(self.x, isX=1)
|
|
for x in r:
|
|
line = Line(x, self.y, x, self.y + h)
|
|
line.strokeColor = self.strokeColor
|
|
line.strokeWidth = self.strokeWidth
|
|
group.add(line)
|
|
elif self.orientation == 'horizontal':
|
|
r = self.makeLinePosList(self.y, isX=0)
|
|
for y in r:
|
|
line = Line(self.x, y, self.x + w, y)
|
|
line.strokeColor = self.strokeColor
|
|
line.strokeWidth = self.strokeWidth
|
|
group.add(line)
|
|
|
|
return group
|
|
|
|
|
|
def makeInnerTiles(self):
|
|
# inner grid lines
|
|
group = Group()
|
|
|
|
w, h = self.width, self.height
|
|
|
|
# inner grid stripes (solid rectangles)
|
|
if self.useRects == 1:
|
|
cols = self.stripeColors
|
|
|
|
if self.orientation == 'vertical':
|
|
r = self.makeLinePosList(self.x, isX=1)
|
|
elif self.orientation == 'horizontal':
|
|
r = self.makeLinePosList(self.y, isX=0)
|
|
|
|
dist = makeDistancesList(r)
|
|
|
|
i = 0
|
|
for j in range(len(dist)):
|
|
if self.orientation == 'vertical':
|
|
x = r[j]
|
|
stripe = Rect(x, self.y, dist[j], h)
|
|
elif self.orientation == 'horizontal':
|
|
y = r[j]
|
|
stripe = Rect(self.x, y, w, dist[j])
|
|
stripe.fillColor = cols[i % len(cols)]
|
|
stripe.strokeColor = None
|
|
group.add(stripe)
|
|
i = i + 1
|
|
|
|
return group
|
|
|
|
|
|
def draw(self):
|
|
# general widget bits
|
|
group = Group()
|
|
|
|
group.add(self.makeOuterRect())
|
|
group.add(self.makeInnerTiles())
|
|
group.add(self.makeInnerLines(),name='_gridLines')
|
|
|
|
return group
|
|
|
|
|
|
class DoubleGrid(Widget):
|
|
"""This combines two ordinary Grid objects orthogonal to each other.
|
|
"""
|
|
|
|
_attrMap = AttrMap(
|
|
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
|
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
|
width = AttrMapValue(isNumber, desc="The grid's width."),
|
|
height = AttrMapValue(isNumber, desc="The grid's height."),
|
|
grid0 = AttrMapValue(None, desc="The first grid component."),
|
|
grid1 = AttrMapValue(None, desc="The second grid component."),
|
|
)
|
|
|
|
def __init__(self):
|
|
self.x = 0
|
|
self.y = 0
|
|
self.width = 100
|
|
self.height = 100
|
|
|
|
g0 = Grid()
|
|
g0.x = self.x
|
|
g0.y = self.y
|
|
g0.width = self.width
|
|
g0.height = self.height
|
|
g0.orientation = 'vertical'
|
|
g0.useLines = 1
|
|
g0.useRects = 0
|
|
g0.delta = 20
|
|
g0.delta0 = 0
|
|
g0.deltaSteps = []
|
|
g0.fillColor = colors.white
|
|
g0.stripeColors = [colors.red, colors.green, colors.blue]
|
|
g0.strokeColor = colors.black
|
|
g0.strokeWidth = 1
|
|
|
|
g1 = Grid()
|
|
g1.x = self.x
|
|
g1.y = self.y
|
|
g1.width = self.width
|
|
g1.height = self.height
|
|
g1.orientation = 'horizontal'
|
|
g1.useLines = 1
|
|
g1.useRects = 0
|
|
g1.delta = 20
|
|
g1.delta0 = 0
|
|
g1.deltaSteps = []
|
|
g1.fillColor = colors.white
|
|
g1.stripeColors = [colors.red, colors.green, colors.blue]
|
|
g1.strokeColor = colors.black
|
|
g1.strokeWidth = 1
|
|
|
|
self.grid0 = g0
|
|
self.grid1 = g1
|
|
|
|
|
|
## # This gives an AttributeError:
|
|
## # DoubleGrid instance has no attribute 'grid0'
|
|
## def __setattr__(self, name, value):
|
|
## if name in ('x', 'y', 'width', 'height'):
|
|
## setattr(self.grid0, name, value)
|
|
## setattr(self.grid1, name, value)
|
|
|
|
|
|
def demo(self):
|
|
D = Drawing(100, 100)
|
|
g = DoubleGrid()
|
|
D.add(g)
|
|
return D
|
|
|
|
|
|
def draw(self):
|
|
group = Group()
|
|
g0, g1 = self.grid0, self.grid1
|
|
# Order groups to make sure both v and h lines
|
|
# are visible (works only when there is only
|
|
# one kind of stripes, v or h).
|
|
G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0)
|
|
for g in G:
|
|
group.add(g.makeOuterRect())
|
|
for g in G:
|
|
group.add(g.makeInnerTiles())
|
|
group.add(g.makeInnerLines(),name='_gridLines')
|
|
|
|
return group
|
|
|
|
|
|
class ShadedRect(Widget):
|
|
"""This makes a rectangle with shaded colors between two colors.
|
|
|
|
Colors are interpolated linearly between 'fillColorStart'
|
|
and 'fillColorEnd', both of which appear at the margins.
|
|
If 'numShades' is set to one, though, only 'fillColorStart'
|
|
is used.
|
|
"""
|
|
|
|
_attrMap = AttrMap(
|
|
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
|
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
|
width = AttrMapValue(isNumber, desc="The grid's width."),
|
|
height = AttrMapValue(isNumber, desc="The grid's height."),
|
|
orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'),
|
|
numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
|
|
fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'),
|
|
fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'),
|
|
strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'),
|
|
strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'),
|
|
cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
|
|
)
|
|
|
|
def __init__(self,**kw):
|
|
self.x = 0
|
|
self.y = 0
|
|
self.width = 100
|
|
self.height = 100
|
|
self.orientation = 'vertical'
|
|
self.numShades = 20
|
|
self.fillColorStart = colors.pink
|
|
self.fillColorEnd = colors.black
|
|
self.strokeColor = colors.black
|
|
self.strokeWidth = 2
|
|
self.cylinderMode = 0
|
|
self.setProperties(kw)
|
|
|
|
def demo(self):
|
|
D = Drawing(100, 100)
|
|
g = ShadedRect()
|
|
D.add(g)
|
|
|
|
return D
|
|
|
|
def _flipRectCorners(self):
|
|
"Flip rectangle's corners if width or height is negative."
|
|
x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd
|
|
if width < 0 and height > 0:
|
|
x = x + width
|
|
width = -width
|
|
if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
|
|
elif height<0 and width>0:
|
|
y = y + height
|
|
height = -height
|
|
if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
|
|
elif height < 0 and height < 0:
|
|
x = x + width
|
|
width = -width
|
|
y = y + height
|
|
height = -height
|
|
return x, y, width, height, fillColorStart, fillColorEnd
|
|
|
|
def draw(self):
|
|
# general widget bits
|
|
group = Group()
|
|
x, y, w, h, c0, c1 = self._flipRectCorners()
|
|
numShades = self.numShades
|
|
if self.cylinderMode:
|
|
if not numShades%2: numShades = numShades+1
|
|
halfNumShades = (numShades-1)/2 + 1
|
|
num = float(numShades) # must make it float!
|
|
vertical = self.orientation == 'vertical'
|
|
if vertical:
|
|
if numShades == 1:
|
|
V = [x]
|
|
else:
|
|
V = frange(x, x + w, w/num)
|
|
else:
|
|
if numShades == 1:
|
|
V = [y]
|
|
else:
|
|
V = frange(y, y + h, h/num)
|
|
|
|
for v in V:
|
|
stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num)
|
|
if self.cylinderMode:
|
|
if V.index(v)>=halfNumShades:
|
|
col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v)
|
|
else:
|
|
col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v)
|
|
else:
|
|
col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v)
|
|
stripe.fillColor = col
|
|
stripe.strokeColor = col
|
|
stripe.strokeWidth = 1
|
|
group.add(stripe)
|
|
if self.strokeColor and self.strokeWidth>=0:
|
|
rect = Rect(x, y, w, h)
|
|
rect.strokeColor = self.strokeColor
|
|
rect.strokeWidth = self.strokeWidth
|
|
rect.fillColor = None
|
|
group.add(rect)
|
|
return group
|
|
|
|
|
|
def colorRange(c0, c1, n):
|
|
"Return a range of intermediate colors between c0 and c1"
|
|
if n==1: return [c0]
|
|
|
|
C = []
|
|
if n>1:
|
|
lim = n-1
|
|
for i in range(n):
|
|
C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i))
|
|
return C
|
|
|
|
|
|
def centroid(P):
|
|
'''compute average point of a set of points'''
|
|
return reduce(lambda x,y, fn=float(len(P)): (x[0]+y[0]/fn,x[1]+y[1]/fn),P,(0,0))
|
|
|
|
def rotatedEnclosingRect(P, angle, rect):
|
|
'''
|
|
given P a sequence P of x,y coordinate pairs and an angle in degrees
|
|
find the centroid of P and the axis at angle theta through it
|
|
find the extreme points of P wrt axis parallel distance and axis
|
|
orthogonal distance. Then compute the least rectangle that will still
|
|
enclose P when rotated by angle.
|
|
|
|
The class R
|
|
'''
|
|
from math import pi, cos, sin, tan
|
|
x0, y0 = centroid(P)
|
|
theta = (angle/180.)*pi
|
|
s,c=sin(theta),cos(theta)
|
|
def parallelAxisDist((x,y),s=s,c=c,x0=x0,y0=y0):
|
|
return (s*(y-y0)+c*(x-x0))
|
|
def orthogonalAxisDist((x,y),s=s,c=c,x0=x0,y0=y0):
|
|
return (c*(y-y0)+s*(x-x0))
|
|
L = map(parallelAxisDist,P)
|
|
L.sort()
|
|
a0, a1 = L[0], L[-1]
|
|
L = map(orthogonalAxisDist,P)
|
|
L.sort()
|
|
b0, b1 = L[0], L[-1]
|
|
rect.x, rect.width = a0, a1-a0
|
|
rect.y, rect.height = b0, b1-b0
|
|
g = Group(transform=(c,s,-s,c,x0,y0))
|
|
g.add(rect)
|
|
return g
|
|
|
|
class ShadedPolygon(Widget,LineShape):
|
|
_attrMap = AttrMap(BASE=LineShape,
|
|
angle = AttrMapValue(isNumber,desc="Shading angle"),
|
|
fillColorStart = AttrMapValue(isColorOrNone),
|
|
fillColorEnd = AttrMapValue(isColorOrNone),
|
|
numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
|
|
cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
|
|
points = AttrMapValue(isListOfNumbers),
|
|
)
|
|
|
|
def __init__(self,**kw):
|
|
self.angle = 90
|
|
self.fillColorStart = colors.red
|
|
self.fillColorEnd = colors.green
|
|
self.cylinderMode = 0
|
|
self.numShades = 50
|
|
self.points = [-1,-1,2,2,3,-1]
|
|
LineShape.__init__(self,kw)
|
|
|
|
def draw(self):
|
|
P = self.points
|
|
P = map(lambda i, P=P:(P[i],P[i+1]),xrange(0,len(P),2))
|
|
path = definePath([('moveTo',)+P[0]]+map(lambda x: ('lineTo',)+x,P[1:])+['closePath'],
|
|
fillColor=None, strokeColor=None)
|
|
path.isClipPath = 1
|
|
g = Group()
|
|
g.add(path)
|
|
rect = ShadedRect(strokeWidth=0,strokeColor=None)
|
|
for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode':
|
|
setattr(rect,k,getattr(self,k))
|
|
g.add(rotatedEnclosingRect(P, self.angle, rect))
|
|
g.add(EmptyClipPath)
|
|
path = path.copy()
|
|
path.isClipPath = 0
|
|
path.strokeColor = self.strokeColor
|
|
path.strokeWidth = self.strokeWidth
|
|
g.add(path)
|
|
return g
|
|
|
|
if __name__=='__main__': #noruntests
|
|
from reportlab.lib.colors import blue
|
|
from reportlab.graphics.shapes import Drawing
|
|
angle=45
|
|
D = Drawing(120,120)
|
|
D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0))
|
|
D.save(formats=['gif'],fnRoot='shobj',outDir='/tmp')
|