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/linecharts.py
5
This modules defines a very preliminary Line Chart example.
7
__version__=''' $Id$ '''
10
from types import FunctionType, StringType
12
from reportlab.lib import colors
13
from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isListOfStrings, \
14
isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \
16
from reportlab.lib.attrmap import *
17
from reportlab.lib.formatters import Formatter
18
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
19
from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine
20
from reportlab.graphics.widgets.signsandsymbols import NoEntry
21
from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis
22
from reportlab.graphics.charts.textlabels import Label
23
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker
24
from reportlab.graphics.charts.areas import PlotArea
26
class LineChartProperties(PropHolder):
28
strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'),
29
strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'),
30
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'),
31
symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.'),
34
class LineChart(PlotArea):
37
# This is conceptually similar to the VerticalBarChart.
38
# Still it is better named HorizontalLineChart... :-/
40
class HorizontalLineChart(LineChart):
41
"""Line chart with multiple lines.
43
A line chart is assumed to have one category and one value axis.
44
Despite its generic name this particular line chart class has
45
a vertical value axis and a horizontal category one. It may
46
evolve into individual horizontal and vertical variants (like
47
with the existing bar charts).
49
Available attributes are:
51
x: x-position of lower-left chart origin
52
y: y-position of lower-left chart origin
56
useAbsolute: disables auto-scaling of chart elements (?)
57
lineLabelNudge: distance of data labels to data points
58
lineLabels: labels associated with data values
59
lineLabelFormat: format string or callback function
60
groupSpacing: space between categories
62
joinedLines: enables drawing of lines
64
strokeColor: color of chart lines (?)
65
fillColor: color for chart background (?)
66
lines: style list, used cyclically for data series
68
valueAxis: value axis object
69
categoryAxis: category axis object
70
categoryNames: category names
72
data: chart data, a list of data series of equal length
75
_attrMap = AttrMap(BASE=LineChart,
76
useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'),
77
lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'),
78
lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
79
lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
80
lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
81
groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
82
joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
83
lines = AttrMapValue(None, desc='Handle of the lines.'),
84
valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
85
categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
86
categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
87
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
88
inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.'),
89
reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'),
90
annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'),
94
LineChart.__init__(self)
96
# Allow for a bounding rectangle.
97
self.strokeColor = None
100
# Named so we have less recoding for the horizontal one :-)
101
self.categoryAxis = XCategoryAxis()
102
self.valueAxis = YValueAxis()
104
# This defines two series of 3 points. Just an example.
105
self.data = [(100,110,120,130),
107
self.categoryNames = ('North','South','East','West')
109
self.lines = TypedPropertyCollection(LineChartProperties)
110
self.lines.strokeWidth = 1
111
self.lines[0].strokeColor = colors.red
112
self.lines[1].strokeColor = colors.green
113
self.lines[2].strokeColor = colors.blue
115
# control spacing. if useAbsolute = 1 then
116
# the next parameters are in points; otherwise
117
# they are 'proportions' and are normalized to
118
# fit the available space.
119
self.useAbsolute = 0 #- not done yet
120
self.groupSpacing = 1 #5
122
self.lineLabels = TypedPropertyCollection(Label)
123
self.lineLabelFormat = None
124
self.lineLabelArray = None
126
# This says whether the origin is above or below
127
# the data point. +10 means put the origin ten points
128
# above the data point if value > 0, or ten
129
# points below if data value < 0. This is different
130
# to label dx/dy which are not dependent on the
132
self.lineLabelNudge = 10
133
# If you have multiple series, by default they butt
136
# New line chart attributes.
137
self.joinedLines = 1 # Connect items with straight lines.
139
self.reversePlotOrder = 0
143
"""Shows basic use of a line chart."""
145
drawing = Drawing(200, 100)
148
(13, 5, 20, 22, 37, 45, 19, 4),
149
(14, 10, 21, 28, 38, 46, 25, 5)
152
lc = HorizontalLineChart()
159
lc.lines.symbol = makeMarker('Circle')
166
def calcPositions(self):
167
"""Works out where they go.
169
Sets an attribute _positions which is a list of
170
lists of (x, y) matching the data.
173
self._seriesCount = len(self.data)
174
self._rowLength = max(map(len,self.data))
177
# Dimensions are absolute.
180
# Dimensions are normalized to fit.
181
normWidth = self.groupSpacing
182
availWidth = self.categoryAxis.scale(0)[1]
183
normFactor = availWidth / normWidth
186
for rowNo in range(len(self.data)):
188
for colNo in range(len(self.data[rowNo])):
189
datum = self.data[rowNo][colNo]
190
if datum is not None:
191
(groupX, groupWidth) = self.categoryAxis.scale(colNo)
192
x = groupX + (0.5 * self.groupSpacing * normFactor)
193
y = self.valueAxis.scale(0)
194
height = self.valueAxis.scale(datum) - y
195
lineRow.append((x, y+height))
196
self._positions.append(lineRow)
199
def _innerDrawLabel(self, rowNo, colNo, x, y):
200
"Draw a label for a given item in the list."
202
labelFmt = self.lineLabelFormat
203
labelValue = self.data[rowNo][colNo]
207
elif type(labelFmt) is StringType:
208
if labelFmt == 'values':
209
labelText = self.lineLabelArray[rowNo][colNo]
211
labelText = labelFmt % labelValue
212
elif type(labelFmt) is FunctionType:
213
labelText = labelFmt(labelValue)
214
elif isinstance(labelFmt, Formatter):
215
labelText = labelFmt(labelValue)
217
msg = "Unknown formatter type %s, expected string or function"
218
raise Exception, msg % labelFmt
221
label = self.lineLabels[(rowNo, colNo)]
222
# Make sure labels are some distance off the data point.
224
label.setOrigin(x, y + self.lineLabelNudge)
226
label.setOrigin(x, y - self.lineLabelNudge)
227
label.setText(labelText)
232
def drawLabel(self, G, rowNo, colNo, x, y):
233
'''Draw a label for a given item in the list.
234
G must have an add method'''
235
G.add(self._innerDrawLabel(rowNo,colNo,x,y))
240
labelFmt = self.lineLabelFormat
241
P = range(len(self._positions))
242
if self.reversePlotOrder: P.reverse()
245
inFillY = self.categoryAxis._y
246
inFillX0 = self.valueAxis._x
247
inFillX1 = inFillX0 + self.categoryAxis._length
248
inFillG = getattr(self,'_inFillG',g)
250
# Iterate over data rows.
252
row = self._positions[rowNo]
253
styleCount = len(self.lines)
254
styleIdx = rowNo % styleCount
255
rowStyle = self.lines[styleIdx]
256
rowColor = rowStyle.strokeColor
257
dash = getattr(rowStyle, 'strokeDashArray', None)
259
if hasattr(self.lines[styleIdx], 'strokeWidth'):
260
strokeWidth = self.lines[styleIdx].strokeWidth
261
elif hasattr(self.lines, 'strokeWidth'):
262
strokeWidth = self.lines.strokeWidth
266
# Iterate over data columns.
269
for colNo in range(len(row)):
272
points = points + [inFillX1,inFillY,inFillX0,inFillY]
273
inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1))
275
line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
277
line.strokeWidth = strokeWidth
279
line.strokeDashArray = dash
282
if hasattr(self.lines[styleIdx], 'symbol'):
283
uSymbol = self.lines[styleIdx].symbol
284
elif hasattr(self.lines, 'symbol'):
285
uSymbol = self.lines.symbol
290
for colNo in range(len(row)):
292
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor)
293
if symbol: g.add(symbol)
296
for colNo in range(len(row)):
298
self.drawLabel(g, rowNo, colNo, x1, y1)
305
vA, cA = self.valueAxis, self.categoryAxis
306
vA.setPosition(self.x, self.y, self.height)
307
if vA: vA.joinAxis = cA
308
if cA: cA.joinAxis = vA
309
vA.configure(self.data)
311
# If zero is in chart, put x axis there, otherwise
313
xAxisCrossesAt = vA.scale(0)
314
if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
319
cA.setPosition(self.x, y, self.width)
320
cA.configure(self.data)
325
g.add(self.makeBackground())
327
self._inFillG = Group()
333
vA.gridEnd = cA._x+cA._length
335
cA.gridEnd = vA._y+vA._length
336
cA.makeGrid(g,parent=self)
337
vA.makeGrid(g,parent=self)
338
g.add(self.makeLines())
339
for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
342
def _cmpFakeItem(a,b):
343
'''t, z0, z1, x, y = a[:5]'''
344
return cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4]))
351
if what: self._data.append(what)
357
self._data.sort(_cmpFakeItem)
358
#for t in self._data: print t
360
class HorizontalLineChart3D(HorizontalLineChart):
361
_attrMap = AttrMap(BASE=HorizontalLineChart,
362
theta_x = AttrMapValue(isNumber, desc='dx/dz'),
363
theta_y = AttrMapValue(isNumber, desc='dy/dz'),
364
zDepth = AttrMapValue(isNumber, desc='depth of an individual series'),
365
zSpace = AttrMapValue(isNumber, desc='z gap around series'),
372
def calcPositions(self):
373
HorizontalLineChart.calcPositions(self)
374
nSeries = self._seriesCount
377
if self.categoryAxis.style=='parallel_3d':
378
_3d_depth = nSeries*zDepth+(nSeries+1)*zSpace
380
_3d_depth = zDepth + 2*zSpace
381
self._3d_dx = self.theta_x*_3d_depth
382
self._3d_dy = self.theta_y*_3d_depth
384
def _calc_z0(self,rowNo):
386
if self.categoryAxis.style=='parallel_3d':
387
z0 = rowNo*(self.zDepth+zSpace)+zSpace
392
def _zadjust(self,x,y,z):
393
return x+z*self.theta_x, y+z*self.theta_y
396
labelFmt = self.lineLabelFormat
397
P = range(len(self._positions))
398
if self.reversePlotOrder: P.reverse()
400
assert not inFill, "inFill not supported for 3d yet"
402
#inFillY = self.categoryAxis._y
403
#inFillX0 = self.valueAxis._x
404
#inFillX1 = inFillX0 + self.categoryAxis._length
405
#inFillG = getattr(self,'_inFillG',g)
407
_zadjust = self._zadjust
408
theta_x = self.theta_x
409
theta_y = self.theta_y
411
from utils3d import _make_3d_line_info
412
tileWidth = getattr(self,'_3d_tilewidth',None)
413
if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1
415
# Iterate over data rows.
417
row = self._positions[rowNo]
419
styleCount = len(self.lines)
420
styleIdx = rowNo % styleCount
421
rowStyle = self.lines[styleIdx]
422
rowColor = rowStyle.strokeColor
423
dash = getattr(rowStyle, 'strokeDashArray', None)
424
z0 = self._calc_z0(rowNo)
427
if hasattr(self.lines[styleIdx], 'strokeWidth'):
428
strokeWidth = self.lines[styleIdx].strokeWidth
429
elif hasattr(self.lines, 'strokeWidth'):
430
strokeWidth = self.lines.strokeWidth
434
# Iterate over data columns.
438
for colNo in xrange(1,n):
440
_make_3d_line_info( F, x0, x1, y0, y1, z0, z1,
442
rowColor, fillColorShaded=None, tileWidth=tileWidth,
443
strokeColor=None, strokeWidth=None, strokeDashArray=None,
447
if hasattr(self.lines[styleIdx], 'symbol'):
448
uSymbol = self.lines[styleIdx].symbol
449
elif hasattr(self.lines, 'symbol'):
450
uSymbol = self.lines.symbol
455
for colNo in xrange(n):
457
x1, y1 = _zadjust(x1,y1,z0)
458
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor)
459
if symbol: F.add((2,z0,z0,x1,y1,symbol))
462
for colNo in xrange(n):
464
x1, y1 = _zadjust(x1,y1,z0)
465
L = self._innerDrawLabel(rowNo, colNo, x1, y1)
466
if L: F.add((2,z0,z0,x1,y1,L))
470
map(lambda x,a=g.add: a(x[-1]),F.value())
473
class VerticalLineChart(LineChart):
478
drawing = Drawing(400, 200)
481
(13, 5, 20, 22, 37, 45, 19, 4),
482
(5, 20, 46, 38, 23, 21, 6, 14)
485
lc = HorizontalLineChart()
493
lc.lines.symbol = makeMarker('FilledDiamond')
494
lc.lineLabelFormat = '%2.0f'
496
catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ')
497
lc.categoryAxis.categoryNames = catNames
498
lc.categoryAxis.labels.boxAnchor = 'n'
500
lc.valueAxis.valueMin = 0
501
lc.valueAxis.valueMax = 60
502
lc.valueAxis.valueStep = 15
509
class SampleHorizontalLineChart(HorizontalLineChart):
510
"Sample class overwriting one method to draw additional horizontal lines."
513
"""Shows basic use of a line chart."""
515
drawing = Drawing(200, 100)
518
(13, 5, 20, 22, 37, 45, 19, 4),
519
(14, 10, 21, 28, 38, 46, 25, 5)
522
lc = SampleHorizontalLineChart()
529
lc.strokeColor = colors.white
530
lc.fillColor = colors.HexColor(0xCCCCCC)
537
def makeBackground(self):
540
g.add(HorizontalLineChart.makeBackground(self))
542
valAxis = self.valueAxis
543
valTickPositions = valAxis._tickValues
545
for y in valTickPositions:
547
g.add(Line(self.x, y, self.x+self.width, y,
548
strokeColor = self.strokeColor))
555
drawing = Drawing(400, 200)
558
(13, 5, 20, 22, 37, 45, 19, 4),
559
(5, 20, 46, 38, 23, 21, 6, 14)
562
lc = SampleHorizontalLineChart()
570
lc.strokeColor = colors.white
571
lc.fillColor = colors.HexColor(0xCCCCCC)
572
lc.lines.symbol = makeMarker('FilledDiamond')
573
lc.lineLabelFormat = '%2.0f'
575
catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ')
576
lc.categoryAxis.categoryNames = catNames
577
lc.categoryAxis.labels.boxAnchor = 'n'
579
lc.valueAxis.valueMin = 0
580
lc.valueAxis.valueMax = 60
581
lc.valueAxis.valueStep = 15
589
drawing = Drawing(400, 200)
592
(13, 5, 20, 22, 37, 45, 19, 4),
593
(5, 20, 46, 38, 23, 21, 6, 14)
596
lc = HorizontalLineChart()
604
lc.lines.symbol = makeMarker('Smiley')
605
lc.lineLabelFormat = '%2.0f'
606
lc.strokeColor = colors.black
607
lc.fillColor = colors.lightblue
609
catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ')
610
lc.categoryAxis.categoryNames = catNames
611
lc.categoryAxis.labels.boxAnchor = 'n'
613
lc.valueAxis.valueMin = 0
614
lc.valueAxis.valueMax = 60
615
lc.valueAxis.valueStep = 15
623
drawing = Drawing(400, 200)
626
(13, 5, 20, 22, 37, 45, 19, 4),
627
(5, 20, 46, 38, 23, 21, 6, 14)
630
lc = HorizontalLineChart()
638
lc.lineLabelFormat = '%2.0f'
639
lc.strokeColor = colors.black
641
lc.lines[0].symbol = makeMarker('Smiley')
642
lc.lines[1].symbol = NoEntry
643
lc.lines[0].strokeWidth = 2
644
lc.lines[1].strokeWidth = 4
646
catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ')
647
lc.categoryAxis.categoryNames = catNames
648
lc.categoryAxis.labels.boxAnchor = 'n'
650
lc.valueAxis.valueMin = 0
651
lc.valueAxis.valueMax = 60
652
lc.valueAxis.valueStep = 15