443 lines
16 KiB
Python
443 lines
16 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/charts/textlabels.py
|
|
__version__=''' $Id: textlabels.py 2647 2005-07-26 13:47:51Z rgbecker $ '''
|
|
import string
|
|
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
|
|
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString
|
|
from reportlab.lib.attrmap import *
|
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
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
|
|
|
|
_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 _simpleSplit(txt,mW,SW):
|
|
L = []
|
|
ws = SW(' ')
|
|
O = []
|
|
w = -ws
|
|
for t in string.split(txt):
|
|
lt = SW(t)
|
|
if w+ws+lt<=mW or O==[]:
|
|
O.append(t)
|
|
w = w + ws + lt
|
|
else:
|
|
L.append(string.join(O,' '))
|
|
O = [t]
|
|
w = lt
|
|
if O!=[]: L.append(string.join(O,' '))
|
|
return L
|
|
|
|
def _pathNumTrunc(n):
|
|
if int(n)==n: return int(n)
|
|
return round(n,5)
|
|
|
|
def _processGlyph(G, truncate=1, pathReverse=0):
|
|
O = []
|
|
P = []
|
|
R = []
|
|
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 xrange(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 = 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='Times-Roman', fontSize=1000,
|
|
anchor='start', truncate=1, pathReverse=0):
|
|
global _gs
|
|
if not _gs:
|
|
import _renderPM
|
|
_gs = _renderPM.gstate(1,1)
|
|
from reportlab.graphics import renderPM
|
|
renderPM._setFont(_gs,fontName,fontSize)
|
|
P = []
|
|
if not anchor =='start':
|
|
textLen = stringWidth(text, fontName,fontSize)
|
|
if text_anchor=='end':
|
|
x = x-textLen
|
|
elif text_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='Times-Roman', fontSize=1000,
|
|
anchor='start', truncate=1, pathReverse=0):
|
|
return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName,
|
|
fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse))
|
|
|
|
_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),
|
|
y = AttrMapValue(isNumber),
|
|
dx = AttrMapValue(isNumber),
|
|
dy = AttrMapValue(isNumber),
|
|
angle = AttrMapValue(isNumber),
|
|
boxAnchor = AttrMapValue(isBoxAnchor),
|
|
boxStrokeColor = AttrMapValue(isColorOrNone),
|
|
boxStrokeWidth = AttrMapValue(isNumber),
|
|
boxFillColor = AttrMapValue(isColorOrNone),
|
|
boxTarget = AttrMapValue(isString),
|
|
fillColor = AttrMapValue(isColorOrNone),
|
|
strokeColor = AttrMapValue(isColorOrNone),
|
|
strokeWidth = AttrMapValue(isNumber),
|
|
text = AttrMapValue(isString),
|
|
fontName = AttrMapValue(isString),
|
|
fontSize = AttrMapValue(isNumber),
|
|
leading = AttrMapValue(isNumberOrNone),
|
|
width = AttrMapValue(isNumberOrNone),
|
|
maxWidth = AttrMapValue(isNumberOrNone),
|
|
height = AttrMapValue(isNumberOrNone),
|
|
textAnchor = AttrMapValue(isTextAnchor),
|
|
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
|
|
topPadding = AttrMapValue(isNumber,'padding at top of box'),
|
|
leftPadding = AttrMapValue(isNumber,'padding at left of box'),
|
|
rightPadding = AttrMapValue(isNumber,'padding at right of box'),
|
|
bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
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._lines = string.split(self._text, '\n')
|
|
self._lineWidths = []
|
|
topPadding = self.topPadding
|
|
leftPadding = self.leftPadding
|
|
rightPadding = self.rightPadding
|
|
bottomPadding = self.bottomPadding
|
|
SW = lambda text, fN=self.fontName, fS=self.fontSize: stringWidth(text, fN, fS)
|
|
if self.maxWidth:
|
|
L = []
|
|
for l in self._lines:
|
|
L[-1:-1] = _simpleSplit(l,self.maxWidth,SW)
|
|
self._lines = L
|
|
if not self.width:
|
|
w = 0
|
|
for line in self._lines:
|
|
thisWidth = SW(line)
|
|
self._lineWidths.append(thisWidth)
|
|
w = max(w,thisWidth)
|
|
self._width = w+leftPadding+rightPadding
|
|
else:
|
|
self._width = self.width
|
|
self._height = self.height or ((self.leading or 1.2*self.fontSize) * 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 draw(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.fontSize
|
|
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 or 1.2*fontSize)
|
|
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 = y - leading
|
|
else:
|
|
for line in self._lines:
|
|
s = String(x, y, line)
|
|
s.textAnchor = textAnchor
|
|
s.fontName = fontName
|
|
s.fontSize = fontSize
|
|
s.fillColor = fillColor
|
|
g.add(s)
|
|
y = y - leading
|
|
|
|
return g
|
|
|
|
class LabelDecorator:
|
|
_attrMap = AttrMap(
|
|
x = AttrMapValue(isNumberOrNone),
|
|
y = AttrMapValue(isNumberOrNone),
|
|
dx = AttrMapValue(isNumberOrNone),
|
|
dy = AttrMapValue(isNumberOrNone),
|
|
angle = AttrMapValue(isNumberOrNone),
|
|
boxAnchor = AttrMapValue(isBoxAnchor),
|
|
boxStrokeColor = AttrMapValue(isColorOrNone),
|
|
boxStrokeWidth = AttrMapValue(isNumberOrNone),
|
|
boxFillColor = AttrMapValue(isColorOrNone),
|
|
fillColor = AttrMapValue(isColorOrNone),
|
|
strokeColor = AttrMapValue(isColorOrNone),
|
|
strokeWidth = AttrMapValue(isNumberOrNone),
|
|
fontName = AttrMapValue(isNoneOrString),
|
|
fontSize = AttrMapValue(isNumberOrNone),
|
|
leading = AttrMapValue(isNumberOrNone),
|
|
width = AttrMapValue(isNumberOrNone),
|
|
maxWidth = AttrMapValue(isNumberOrNone),
|
|
height = AttrMapValue(isNumberOrNone),
|
|
textAnchor = AttrMapValue(isTextAnchor),
|
|
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 BarChartLabel(Label):
|
|
"""
|
|
An extended Label allowing for nudging, lines visibility etc
|
|
"""
|
|
_attrMap = AttrMap(
|
|
BASE=Label,
|
|
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"),
|
|
)
|
|
|
|
def __init__(self):
|
|
Label.__init__(self)
|
|
self.lineStrokeWidth = 0
|
|
self.lineStrokeColor = None
|
|
self.nudge = 0
|
|
self.fixedStart = self.fixedEnd = None
|
|
self._pmv = 0
|
|
|
|
def _getBoxAnchor(self):
|
|
a = self.boxAnchor
|
|
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 = self.textAnchor
|
|
if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
|
|
return a
|
|
|
|
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))
|