1
#Copyright ReportLab Europe Ltd. 2000-2004
2
#see license.txt for license details
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py
4
__version__=''' $Id$ '''
7
from reportlab.lib import colors
8
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
9
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString
10
from reportlab.lib.attrmap import *
11
from reportlab.pdfbase.pdfmetrics import stringWidth
12
from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS
13
from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath
14
from reportlab.graphics.widgetbase import Widget, PropHolder
18
'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'},
19
'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'},
21
def _simpleSplit(txt,mW,SW):
26
for t in string.split(txt):
28
if w+ws+lt<=mW or O==[]:
32
L.append(string.join(O,' '))
35
if O!=[]: L.append(string.join(O,' '))
39
if int(n)==n: return int(n)
42
def _processGlyph(G, truncate=1, pathReverse=0):
46
for g in G+(('end',),):
48
if O and op in ['moveTo', 'moveToClosed','end']:
49
if O[0]=='moveToClosed':
52
for i in xrange(0,len(P),2):
53
P[i+1], P[i] = P[i:i+2]
59
if truncate: P = map(_pathNumTrunc,P)
61
j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)]
65
R.append((o,)+ tuple(P[i:j]))
73
def _text2PathDescription(text, x=0, y=0, fontName='Times-Roman', fontSize=1000,
74
anchor='start', truncate=1, pathReverse=0):
78
_gs = _renderPM.gstate(1,1)
79
from reportlab.graphics import renderPM
80
renderPM._setFont(_gs,fontName,fontSize)
82
if not anchor =='start':
83
textLen = stringWidth(text, fontName,fontSize)
84
if text_anchor=='end':
86
elif text_anchor=='middle':
88
for g in _gs._stringPath(text,x,y):
89
P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse))
92
def _text2Path(text, x=0, y=0, fontName='Times-Roman', fontSize=1000,
93
anchor='start', truncate=1, pathReverse=0):
94
return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName,
95
fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse))
97
_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'}
99
"""A text label to attach to something else, such as a chart axis.
101
This allows you to specify an offset, angle and many anchor
102
properties relative to the label's origin. It allows, for example,
103
angled multiline axis labels.
105
# fairly straight port of Robin Becker's textbox.py to new widgets
109
x = AttrMapValue(isNumber),
110
y = AttrMapValue(isNumber),
111
dx = AttrMapValue(isNumber),
112
dy = AttrMapValue(isNumber),
113
angle = AttrMapValue(isNumber),
114
boxAnchor = AttrMapValue(isBoxAnchor),
115
boxStrokeColor = AttrMapValue(isColorOrNone),
116
boxStrokeWidth = AttrMapValue(isNumber),
117
boxFillColor = AttrMapValue(isColorOrNone),
118
fillColor = AttrMapValue(isColorOrNone),
119
strokeColor = AttrMapValue(isColorOrNone),
120
strokeWidth = AttrMapValue(isNumber),
121
text = AttrMapValue(isString),
122
fontName = AttrMapValue(isString),
123
fontSize = AttrMapValue(isNumber),
124
leading = AttrMapValue(isNumberOrNone),
125
width = AttrMapValue(isNumberOrNone),
126
maxWidth = AttrMapValue(isNumberOrNone),
127
height = AttrMapValue(isNumberOrNone),
128
textAnchor = AttrMapValue(isTextAnchor),
129
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
130
topPadding = AttrMapValue(isNumber,'padding at top of box'),
131
leftPadding = AttrMapValue(isNumber,'padding at left of box'),
132
rightPadding = AttrMapValue(isNumber,'padding at right of box'),
133
bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
136
def __init__(self,**kw):
137
self._setKeywords(**kw)
139
_text = 'Multi-Line\nString',
150
boxStrokeWidth = 0.5,
151
boxStrokeColor = None,
158
fillColor = STATE_DEFAULTS['fillColor'],
159
fontName = STATE_DEFAULTS['fontName'],
160
fontSize = STATE_DEFAULTS['fontSize'],
162
textAnchor = 'start',
166
def setText(self, text):
167
"""Set the text property. May contain embedded newline characters.
168
Called by the containing chart or axis."""
172
def setOrigin(self, x, y):
173
"""Set the origin. This would be the tick mark or bar top relative to
174
which it is defined. Called by the containing chart or axis."""
180
"""This shows a label positioned with its top right corner
181
at the top centre of the drawing, and rotated 45 degrees."""
183
d = Drawing(200, 100)
185
# mark the origin of the label
186
d.add(Circle(100,90, 5, fillColor=colors.green))
189
lab.setOrigin(100,90)
194
lab.boxStrokeColor = colors.green
195
lab.setText('Another\nMulti-Line\nString')
200
def _getBoxAnchor(self):
201
'''hook for allowing special box anchor effects'''
203
if ba in ('autox', 'autoy'):
205
na = (int((angle%360)/45.)*45)%360
206
if not (na % 90): # we have a right angle case
207
da = (angle - na) % 360
209
na = na + (da>0 and 45 or -45)
210
ba = _A2BA[ba[-1]][na]
213
def computeSize(self):
214
# the thing will draw in its own coordinate system
215
self._lines = string.split(self._text, '\n')
216
self._lineWidths = []
217
topPadding = self.topPadding
218
leftPadding = self.leftPadding
219
rightPadding = self.rightPadding
220
bottomPadding = self.bottomPadding
221
SW = lambda text, fN=self.fontName, fS=self.fontSize: stringWidth(text, fN, fS)
224
for l in self._lines:
225
L[-1:-1] = _simpleSplit(l,self.maxWidth,SW)
229
for line in self._lines:
231
self._lineWidths.append(thisWidth)
233
self._width = w+leftPadding+rightPadding
235
self._width = self.width
236
self._height = self.height or ((self.leading or 1.2*self.fontSize) * len(self._lines)+topPadding+bottomPadding)
237
self._ewidth = (self._width-leftPadding-rightPadding)
238
self._eheight = (self._height-topPadding-bottomPadding)
239
boxAnchor = self._getBoxAnchor()
240
if boxAnchor in ['n','ne','nw']:
241
self._top = -topPadding
242
elif boxAnchor in ['s','sw','se']:
243
self._top = self._height-topPadding
245
self._top = 0.5*self._eheight
246
self._bottom = self._top - self._eheight
248
if boxAnchor in ['ne','e','se']:
249
self._left = leftPadding - self._width
250
elif boxAnchor in ['nw','w','sw']:
251
self._left = leftPadding
253
self._left = -self._ewidth*0.5
254
self._right = self._left+self._ewidth
256
def _getTextAnchor(self):
257
'''This can be overridden to allow special effects'''
259
if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()]
264
self._text = _text or ''
268
g.translate(self.x + self.dx, self.y + self.dy)
271
y = self._top - self.fontSize
272
textAnchor = self._getTextAnchor()
273
if textAnchor == 'start':
275
elif textAnchor == 'middle':
276
x = self._left + self._ewidth*0.5
280
# paint box behind text just in case they
282
if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
283
g.add(Rect( self._left-self.leftPadding,
284
self._bottom-self.bottomPadding,
287
strokeColor=self.boxStrokeColor,
288
strokeWidth=self.boxStrokeWidth,
289
fillColor=self.boxFillColor)
292
fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize
293
strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, (self.leading or 1.2*fontSize)
295
for line in self._lines:
296
s = _text2Path(line, x, y, fontName, fontSize, textAnchor)
297
s.fillColor = fillColor
298
s.strokeColor = strokeColor
299
s.strokeWidth = strokeWidth
303
for line in self._lines:
304
s = String(x, y, line)
305
s.textAnchor = textAnchor
306
s.fontName = fontName
307
s.fontSize = fontSize
308
s.fillColor = fillColor
314
class LabelDecorator:
316
x = AttrMapValue(isNumberOrNone),
317
y = AttrMapValue(isNumberOrNone),
318
dx = AttrMapValue(isNumberOrNone),
319
dy = AttrMapValue(isNumberOrNone),
320
angle = AttrMapValue(isNumberOrNone),
321
boxAnchor = AttrMapValue(isBoxAnchor),
322
boxStrokeColor = AttrMapValue(isColorOrNone),
323
boxStrokeWidth = AttrMapValue(isNumberOrNone),
324
boxFillColor = AttrMapValue(isColorOrNone),
325
fillColor = AttrMapValue(isColorOrNone),
326
strokeColor = AttrMapValue(isColorOrNone),
327
strokeWidth = AttrMapValue(isNumberOrNone),
328
fontName = AttrMapValue(isNoneOrString),
329
fontSize = AttrMapValue(isNumberOrNone),
330
leading = AttrMapValue(isNumberOrNone),
331
width = AttrMapValue(isNumberOrNone),
332
maxWidth = AttrMapValue(isNumberOrNone),
333
height = AttrMapValue(isNumberOrNone),
334
textAnchor = AttrMapValue(isTextAnchor),
335
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
339
self.textAnchor = 'start'
341
for a in self._attrMap.keys():
342
if not hasattr(self,a): setattr(self,a,None)
344
def decorate(self,l,L):
345
chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo
346
L.setText(chart.categoryAxis.categoryNames[colNo])
349
def __call__(self,l):
350
from copy import deepcopy
352
for a,v in self.__dict__.items():
353
if v is None: v = getattr(l,a,None)
357
isOffsetMode=OneOf('high','low','bar','axis')
358
class LabelOffset(PropHolder):
360
posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"),
361
pos = AttrMapValue(isNumber,desc='Value for positive elements'),
362
negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"),
363
neg = AttrMapValue(isNumber,desc='Value for negative elements'),
366
self.posMode=self.negMode='axis'
367
self.pos = self.neg = 0
369
def _getValue(self, chart, val):
370
flipXY = chart._flipXY
371
A = chart.categoryAxis
396
NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset))
398
class BarChartLabel(Label):
400
An extended Label allowing for nudging, lines visibility etc
404
lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"),
405
lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"),
406
fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"),
407
fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"),
408
nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"),
413
self.lineStrokeWidth = 0
414
self.lineStrokeColor = None
416
self.fixedStart = self.fixedEnd = None
419
def _getBoxAnchor(self):
421
if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a]
424
def _getTextAnchor(self):
426
if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
429
class NA_Label(BarChartLabel):
431
An extended Label allowing for nudging, lines visibility etc
435
text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"),
438
BarChartLabel.__init__(self)
440
NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label))