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/spider.py
4
# spider chart, also known as radar chart
8
Normal use shows variation of 5-10 parameters against some 'norm' or target.
9
When there is more than one series, place the series with the largest
10
numbers first, as it will be overdrawn by each successive one.
12
__version__=''' $Id$ '''
15
from math import sin, cos, pi
17
from reportlab.lib import colors
18
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
19
isListOfNumbers, isColorOrNone, isString,\
20
isListOfStringsOrNone, OneOf, SequenceOf,\
21
isBoolean, isListOfColors, isNumberOrNone,\
22
isNoneOrListOfNoneOrStrings, isTextAnchor,\
23
isNoneOrListOfNoneOrNumbers, isBoxAnchor,\
25
from reportlab.lib.attrmap import *
26
from reportlab.pdfgen.canvas import Canvas
27
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \
28
Wedge, String, STATE_DEFAULTS
29
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
30
from reportlab.graphics.charts.areas import PlotArea
31
from piecharts import WedgeLabel
32
from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol
34
class StrandProperties(PropHolder):
35
"""This holds descriptive information about concentric 'strands'.
37
Line style, whether filled etc.
41
strokeWidth = AttrMapValue(isNumber),
42
fillColor = AttrMapValue(isColorOrNone),
43
strokeColor = AttrMapValue(isColorOrNone),
44
strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
45
fontName = AttrMapValue(isString),
46
fontSize = AttrMapValue(isNumber),
47
fontColor = AttrMapValue(isColorOrNone),
48
labelRadius = AttrMapValue(isNumber),
49
markers = AttrMapValue(isBoolean),
50
markerType = AttrMapValue(isAnything),
51
markerSize = AttrMapValue(isNumber),
52
label_dx = AttrMapValue(isNumber),
53
label_dy = AttrMapValue(isNumber),
54
label_angle = AttrMapValue(isNumber),
55
label_boxAnchor = AttrMapValue(isBoxAnchor),
56
label_boxStrokeColor = AttrMapValue(isColorOrNone),
57
label_boxStrokeWidth = AttrMapValue(isNumber),
58
label_boxFillColor = AttrMapValue(isColorOrNone),
59
label_strokeColor = AttrMapValue(isColorOrNone),
60
label_strokeWidth = AttrMapValue(isNumber),
61
label_text = AttrMapValue(isStringOrNone),
62
label_leading = AttrMapValue(isNumberOrNone),
63
label_width = AttrMapValue(isNumberOrNone),
64
label_maxWidth = AttrMapValue(isNumberOrNone),
65
label_height = AttrMapValue(isNumberOrNone),
66
label_textAnchor = AttrMapValue(isTextAnchor),
67
label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
68
label_topPadding = AttrMapValue(isNumber,'padding at top of box'),
69
label_leftPadding = AttrMapValue(isNumber,'padding at left of box'),
70
label_rightPadding = AttrMapValue(isNumber,'padding at right of box'),
71
label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
77
self.strokeColor = STATE_DEFAULTS["strokeColor"]
78
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
79
self.fontName = STATE_DEFAULTS["fontName"]
80
self.fontSize = STATE_DEFAULTS["fontSize"]
81
self.fontColor = STATE_DEFAULTS["fillColor"]
82
self.labelRadius = 1.2
84
self.markerType = None
86
self.label_dx = self.label_dy = self.label_angle = 0
87
self.label_text = None
88
self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0
89
self.label_boxAnchor = 'c'
90
self.label_boxStrokeColor = None #boxStroke
91
self.label_boxStrokeWidth = 0.5 #boxStrokeWidth
92
self.label_boxFillColor = None
93
self.label_strokeColor = None
94
self.label_strokeWidth = 0.1
95
self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None
96
self.label_textAnchor = 'start'
97
self.label_visible = 1
99
class SpiderChart(PlotArea):
100
_attrMap = AttrMap(BASE=PlotArea,
101
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
102
labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
103
startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
104
direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
105
strands = AttrMapValue(None, desc="collection of strand descriptor objects"),
109
PlotArea.__init__(self)
111
self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]]
112
self.labels = None # or list of strings
114
self.direction = "clockwise"
116
self.strands = TypedPropertyCollection(StrandProperties)
117
self.strands[0].fillColor = colors.cornsilk
118
self.strands[1].fillColor = colors.cyan
122
d = Drawing(200, 100)
129
sp.data = [[10,12,14,16,18,20],[6,8,4,6,8,10]]
130
sp.labels = ['a','b','c','d','e','f']
135
def normalizeData(self, outer = 0.0):
136
"""Turns data into normalized ones where each datum is < 1.0,
137
and 1.0 = maximum radius. Adds 10% at outside edge by default"""
142
assert element >=0, "Cannot do spider plots of negative numbers!"
145
theMax = theMax * (1.0+outer)
151
scaledRow.append(element / theMax)
152
scaled.append(scaledRow)
157
# normalize slice data
158
g = self.makeBackground() or Group()
160
xradius = self.width/2.0
161
yradius = self.height/2.0
162
self._radius = radius = min(xradius, yradius)
163
centerx = self.x + xradius
164
centery = self.y + yradius
166
data = self.normalizeData()
171
if self.labels is None:
175
#there's no point in raising errors for less than enough errors if
176
#we silently create all for the extreme case of no labels.
179
labels = labels + ['']*i
183
angle = self.startAngle*pi/180
184
direction = self.direction == "clockwise" and -1 or 1
185
angleBetween = direction*(2 * pi)/n
186
markers = self.strands.markers
188
car = cos(angle)*radius
189
sar = sin(angle)*radius
190
csa.append((car,sar,angle))
191
spoke = Line(centerx, centery, centerx + car, centery + sar, strokeWidth = 0.5)
192
#print 'added spoke (%0.2f, %0.2f) -> (%0.2f, %0.2f)' % (spoke.x1, spoke.y1, spoke.x2, spoke.y2)
197
if text is None: text = labels[i]
199
labelRadius = si.labelRadius
201
L.x = centerx + labelRadius*car
202
L.y = centery + labelRadius*sar
203
L.boxAnchor = si.label_boxAnchor
204
L._pmv = angle*180/pi
207
L.angle = si.label_angle
208
L.boxAnchor = si.label_boxAnchor
209
L.boxStrokeColor = si.label_boxStrokeColor
210
L.boxStrokeWidth = si.label_boxStrokeWidth
211
L.boxFillColor = si.label_boxFillColor
212
L.strokeColor = si.label_strokeColor
213
L.strokeWidth = si.label_strokeWidth
215
L.leading = si.label_leading
216
L.width = si.label_width
217
L.maxWidth = si.label_maxWidth
218
L.height = si.label_height
219
L.textAnchor = si.label_textAnchor
220
L.visible = si.label_visible
221
L.topPadding = si.label_topPadding
222
L.leftPadding = si.label_leftPadding
223
L.rightPadding = si.label_rightPadding
224
L.bottomPadding = si.label_bottomPadding
225
L.fontName = si.fontName
226
L.fontSize = si.fontSize
227
L.fillColor = si.fontColor
229
angle = angle + angleBetween
231
# now plot the polygons
237
car, sar = csa[-1][:2]
239
points.append(centerx+car*r)
240
points.append(centery+sar*r)
242
car, sar = csa[i][:2]
244
points.append(centerx+car*r)
245
points.append(centery+sar*r)
247
# make up the 'strand'
248
strand = Polygon(points)
249
strand.fillColor = self.strands[rowIdx].fillColor
250
strand.strokeColor = self.strands[rowIdx].strokeColor
251
strand.strokeWidth = self.strands[rowIdx].strokeWidth
252
strand.strokeDashArray = self.strands[rowIdx].strokeDashArray
256
# put in a marker, if it needs one
258
if hasattr(self.strands[rowIdx], 'markerType'):
259
uSymbol = self.strands[rowIdx].markerType
260
elif hasattr(self.strands, 'markerType'):
261
uSymbol = self.strands.markerType
266
m_size = self.strands[rowIdx].markerSize
267
m_fillColor = self.strands[rowIdx].fillColor
268
m_strokeColor = self.strands[rowIdx].strokeColor
269
m_strokeWidth = self.strands[rowIdx].strokeWidth
271
if type(uSymbol) is type(''):
272
symbol = makeMarker(uSymbol,
276
fillColor = m_fillColor,
277
strokeColor = m_strokeColor,
278
strokeWidth = m_strokeWidth,
282
symbol = uSymbol2Symbol(uSymbol,m_x,m_y,m_fillColor)
283
for k,v in (('size', m_size), ('fillColor', m_fillColor),
284
('x', m_x), ('y', m_y),
285
('strokeColor',m_strokeColor), ('strokeWidth',m_strokeWidth),
295
# spokes go over strands
301
"Make a simple spider chart"
303
d = Drawing(400, 400)
310
pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]]
311
pc.labels = ['a','b','c','d','e','f']
312
pc.strands[2].fillColor=colors.palegreen
320
"Make a spider chart with markers, but no fill"
322
d = Drawing(400, 400)
329
pc.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8,3]]
330
pc.labels = ['U','V','W','X','Y','Z']
331
pc.strands.strokeWidth = 2
332
pc.strands[0].fillColor = None
333
pc.strands[1].fillColor = None
334
pc.strands[2].fillColor = None
335
pc.strands[0].strokeColor = colors.red
336
pc.strands[1].strokeColor = colors.blue
337
pc.strands[2].strokeColor = colors.green
338
pc.strands.markers = 1
339
pc.strands.markerType = "FilledDiamond"
340
pc.strands.markerSize = 6
347
if __name__=='__main__':
349
from reportlab.graphics.renderPDF import drawToFile
350
drawToFile(d, 'spider.pdf')
352
drawToFile(d, 'spider2.pdf')
353
#print 'saved spider.pdf'