~openerp-groupes/openobject-server/6.0-fix-setup-windows

« back to all changes in this revision

Viewing changes to bin/reportlab/graphics/charts/piecharts.py

  • Committer: pinky
  • Date: 2006-12-07 13:41:40 UTC
  • Revision ID: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
New trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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/piecharts.py
 
4
# experimental pie chart script.  Two types of pie - one is a monolithic
 
5
#widget with all top-level properties, the other delegates most stuff to
 
6
#a wedges collection whic lets you customize the group or every individual
 
7
#wedge.
 
8
 
 
9
"""Basic Pie Chart class.
 
10
 
 
11
This permits you to customize and pop out individual wedges;
 
12
supports elliptical and circular pies.
 
13
"""
 
14
__version__=''' $Id$ '''
 
15
 
 
16
import copy
 
17
from math import sin, cos, pi
 
18
 
 
19
from reportlab.lib import colors
 
20
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
 
21
                                    isListOfNumbers, isColorOrNone, isString,\
 
22
                                    isListOfStringsOrNone, OneOf, SequenceOf,\
 
23
                                    isBoolean, isListOfColors, isNumberOrNone,\
 
24
                                    isNoneOrListOfNoneOrStrings, isTextAnchor,\
 
25
                                    isNoneOrListOfNoneOrNumbers, isBoxAnchor,\
 
26
                                    isStringOrNone
 
27
from reportlab.lib.attrmap import *
 
28
from reportlab.pdfgen.canvas import Canvas
 
29
from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, ArcPath, Polygon
 
30
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
 
31
from textlabels import Label
 
32
 
 
33
_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'}
 
34
class WedgeLabel(Label):
 
35
    def _checkDXY(self,ba):
 
36
        pass
 
37
    def _getBoxAnchor(self):
 
38
        na = (int((self._pmv%360)/45.)*45)%360
 
39
        if not (na % 90): # we have a right angle case
 
40
            da = (self._pmv - na) % 360
 
41
            if abs(da)>5:
 
42
                na = na + (da>0 and 45 or -45)
 
43
        ba = _ANGLE2BOXANCHOR[na]
 
44
        self._checkDXY(ba)
 
45
        return ba
 
46
 
 
47
class WedgeProperties(PropHolder):
 
48
    """This holds descriptive information about the wedges in a pie chart.
 
49
 
 
50
    It is not to be confused with the 'wedge itself'; this just holds
 
51
    a recipe for how to format one, and does not allow you to hack the
 
52
    angles.  It can format a genuine Wedge object for you with its
 
53
    format method.
 
54
    """
 
55
 
 
56
    _attrMap = AttrMap(
 
57
        strokeWidth = AttrMapValue(isNumber),
 
58
        fillColor = AttrMapValue(isColorOrNone),
 
59
        strokeColor = AttrMapValue(isColorOrNone),
 
60
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
 
61
        popout = AttrMapValue(isNumber),
 
62
        fontName = AttrMapValue(isString),
 
63
        fontSize = AttrMapValue(isNumber),
 
64
        fontColor = AttrMapValue(isColorOrNone),
 
65
        labelRadius = AttrMapValue(isNumber),
 
66
        label_dx = AttrMapValue(isNumber),
 
67
        label_dy = AttrMapValue(isNumber),
 
68
        label_angle = AttrMapValue(isNumber),
 
69
        label_boxAnchor = AttrMapValue(isBoxAnchor),
 
70
        label_boxStrokeColor = AttrMapValue(isColorOrNone),
 
71
        label_boxStrokeWidth = AttrMapValue(isNumber),
 
72
        label_boxFillColor = AttrMapValue(isColorOrNone),
 
73
        label_strokeColor = AttrMapValue(isColorOrNone),
 
74
        label_strokeWidth = AttrMapValue(isNumber),
 
75
        label_text = AttrMapValue(isStringOrNone),
 
76
        label_leading = AttrMapValue(isNumberOrNone),
 
77
        label_width = AttrMapValue(isNumberOrNone),
 
78
        label_maxWidth = AttrMapValue(isNumberOrNone),
 
79
        label_height = AttrMapValue(isNumberOrNone),
 
80
        label_textAnchor = AttrMapValue(isTextAnchor),
 
81
        label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
 
82
        label_topPadding = AttrMapValue(isNumber,'padding at top of box'),
 
83
        label_leftPadding = AttrMapValue(isNumber,'padding at left of box'),
 
84
        label_rightPadding = AttrMapValue(isNumber,'padding at right of box'),
 
85
        label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
 
86
        )
 
87
 
 
88
    def __init__(self):
 
89
        self.strokeWidth = 0
 
90
        self.fillColor = None
 
91
        self.strokeColor = STATE_DEFAULTS["strokeColor"]
 
92
        self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
 
93
        self.popout = 0
 
94
        self.fontName = STATE_DEFAULTS["fontName"]
 
95
        self.fontSize = STATE_DEFAULTS["fontSize"]
 
96
        self.fontColor = STATE_DEFAULTS["fillColor"]
 
97
        self.labelRadius = 1.2
 
98
        self.label_dx = self.label_dy = self.label_angle = 0
 
99
        self.label_text = None
 
100
        self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0
 
101
        self.label_boxAnchor = 'c'
 
102
        self.label_boxStrokeColor = None    #boxStroke
 
103
        self.label_boxStrokeWidth = 0.5 #boxStrokeWidth
 
104
        self.label_boxFillColor = None
 
105
        self.label_strokeColor = None
 
106
        self.label_strokeWidth = 0.1
 
107
        self.label_leading =    self.label_width = self.label_maxWidth = self.label_height = None
 
108
        self.label_textAnchor = 'start'
 
109
        self.label_visible = 1
 
110
 
 
111
def _addWedgeLabel(self,text,add,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel):
 
112
    # now draw a label
 
113
    if self.simpleLabels:
 
114
        theLabel = String(labelX, labelY, text)
 
115
        theLabel.textAnchor = "middle"
 
116
    else:
 
117
        theLabel = labelClass()
 
118
        theLabel._pmv = angle
 
119
        theLabel.x = labelX
 
120
        theLabel.y = labelY
 
121
        theLabel.dx = wedgeStyle.label_dx
 
122
        theLabel.dy = wedgeStyle.label_dy
 
123
        theLabel.angle = wedgeStyle.label_angle
 
124
        theLabel.boxAnchor = wedgeStyle.label_boxAnchor
 
125
        theLabel.boxStrokeColor = wedgeStyle.label_boxStrokeColor
 
126
        theLabel.boxStrokeWidth = wedgeStyle.label_boxStrokeWidth
 
127
        theLabel.boxFillColor = wedgeStyle.label_boxFillColor
 
128
        theLabel.strokeColor = wedgeStyle.label_strokeColor
 
129
        theLabel.strokeWidth = wedgeStyle.label_strokeWidth
 
130
        _text = wedgeStyle.label_text
 
131
        if _text is None: _text = text
 
132
        theLabel._text = _text
 
133
        theLabel.leading = wedgeStyle.label_leading
 
134
        theLabel.width = wedgeStyle.label_width
 
135
        theLabel.maxWidth = wedgeStyle.label_maxWidth
 
136
        theLabel.height = wedgeStyle.label_height
 
137
        theLabel.textAnchor = wedgeStyle.label_textAnchor
 
138
        theLabel.visible = wedgeStyle.label_visible
 
139
        theLabel.topPadding = wedgeStyle.label_topPadding
 
140
        theLabel.leftPadding = wedgeStyle.label_leftPadding
 
141
        theLabel.rightPadding = wedgeStyle.label_rightPadding
 
142
        theLabel.bottomPadding = wedgeStyle.label_bottomPadding
 
143
    theLabel.fontSize = wedgeStyle.fontSize
 
144
    theLabel.fontName = wedgeStyle.fontName
 
145
    theLabel.fillColor = wedgeStyle.fontColor
 
146
    add(theLabel)
 
147
 
 
148
def _fixLabels(labels,n):
 
149
    if labels is None:
 
150
        labels = [''] * n
 
151
    else:
 
152
        i = n-len(labels)
 
153
        if i>0: labels = labels + ['']*i
 
154
    return labels
 
155
 
 
156
class Pie(Widget):
 
157
    _attrMap = AttrMap(
 
158
        x = AttrMapValue(isNumber, desc='X position of the chart within its container.'),
 
159
        y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'),
 
160
        width = AttrMapValue(isNumber, desc='width of pie bounding box. Need not be same as width.'),
 
161
        height = AttrMapValue(isNumber, desc='height of pie bounding box.  Need not be same as height.'),
 
162
        data = AttrMapValue(isListOfNumbers, desc='list of numbers defining wedge sizes; need not sum to 1'),
 
163
        labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
 
164
        startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
 
165
        direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
 
166
        slices = AttrMapValue(None, desc="collection of wedge descriptor objects"),
 
167
        simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"),
 
168
        other_threshold = AttrMapValue(isNumber, desc='A value for doing thresh holding, not used yet.'),
 
169
        )
 
170
    other_threshold=None
 
171
 
 
172
    def __init__(self):
 
173
        self.x = 0
 
174
        self.y = 0
 
175
        self.width = 100
 
176
        self.height = 100
 
177
        self.data = [1]
 
178
        self.labels = None  # or list of strings
 
179
        self.startAngle = 90
 
180
        self.direction = "clockwise"
 
181
        self.simpleLabels = 1
 
182
 
 
183
        self.slices = TypedPropertyCollection(WedgeProperties)
 
184
        self.slices[0].fillColor = colors.darkcyan
 
185
        self.slices[1].fillColor = colors.blueviolet
 
186
        self.slices[2].fillColor = colors.blue
 
187
        self.slices[3].fillColor = colors.cyan
 
188
 
 
189
    def demo(self):
 
190
        d = Drawing(200, 100)
 
191
 
 
192
        pc = Pie()
 
193
        pc.x = 50
 
194
        pc.y = 10
 
195
        pc.width = 100
 
196
        pc.height = 80
 
197
        pc.data = [10,20,30,40,50,60]
 
198
        pc.labels = ['a','b','c','d','e','f']
 
199
 
 
200
        pc.slices.strokeWidth=0.5
 
201
        pc.slices[3].popout = 10
 
202
        pc.slices[3].strokeWidth = 2
 
203
        pc.slices[3].strokeDashArray = [2,2]
 
204
        pc.slices[3].labelRadius = 1.75
 
205
        pc.slices[3].fontColor = colors.red
 
206
        pc.slices[0].fillColor = colors.darkcyan
 
207
        pc.slices[1].fillColor = colors.blueviolet
 
208
        pc.slices[2].fillColor = colors.blue
 
209
        pc.slices[3].fillColor = colors.cyan
 
210
        pc.slices[4].fillColor = colors.aquamarine
 
211
        pc.slices[5].fillColor = colors.cadetblue
 
212
        pc.slices[6].fillColor = colors.lightcoral
 
213
 
 
214
        d.add(pc)
 
215
        return d
 
216
 
 
217
    def normalizeData(self):
 
218
        from operator import add
 
219
        data = self.data
 
220
        self._sum = sum = float(reduce(add,data,0))
 
221
        return abs(sum)>=1e-8 and map(lambda x,f=360./sum: f*x, data) or len(data)*[0]
 
222
 
 
223
    def makeWedges(self):
 
224
        # normalize slice data
 
225
        normData = self.normalizeData()
 
226
        n = len(normData)
 
227
        labels = _fixLabels(self.labels,n)
 
228
 
 
229
        xradius = self.width/2.0
 
230
        yradius = self.height/2.0
 
231
        centerx = self.x + xradius
 
232
        centery = self.y + yradius
 
233
 
 
234
        if self.direction == "anticlockwise":
 
235
            whichWay = 1
 
236
        else:
 
237
            whichWay = -1
 
238
 
 
239
        g = Group()
 
240
        i = 0
 
241
        styleCount = len(self.slices)
 
242
 
 
243
        startAngle = self.startAngle #% 360
 
244
        for angle in normData:
 
245
            endAngle = (startAngle + (angle * whichWay)) #% 360
 
246
            if abs(startAngle-endAngle)>=1e-5:
 
247
                if startAngle < endAngle:
 
248
                    a1 = startAngle
 
249
                    a2 = endAngle
 
250
                else:
 
251
                    a1 = endAngle
 
252
                    a2 = startAngle
 
253
 
 
254
                #if we didn't use %stylecount here we'd end up with the later wedges
 
255
                #all having the default style
 
256
                wedgeStyle = self.slices[i%styleCount]
 
257
 
 
258
                # is it a popout?
 
259
                cx, cy = centerx, centery
 
260
                if wedgeStyle.popout <> 0:
 
261
                    # pop out the wedge
 
262
                    averageAngle = (a1+a2)/2.0
 
263
                    aveAngleRadians = averageAngle * pi/180.0
 
264
                    popdistance = wedgeStyle.popout
 
265
                    cx = centerx + popdistance * cos(aveAngleRadians)
 
266
                    cy = centery + popdistance * sin(aveAngleRadians)
 
267
 
 
268
                if n > 1:
 
269
                    theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius)
 
270
                elif n==1:
 
271
                    theWedge = Ellipse(cx, cy, xradius, yradius)
 
272
 
 
273
                theWedge.fillColor = wedgeStyle.fillColor
 
274
                theWedge.strokeColor = wedgeStyle.strokeColor
 
275
                theWedge.strokeWidth = wedgeStyle.strokeWidth
 
276
                theWedge.strokeDashArray = wedgeStyle.strokeDashArray
 
277
 
 
278
                g.add(theWedge)
 
279
                text = labels[i]
 
280
                if text:
 
281
                    averageAngle = (a1+a2)/2.0
 
282
                    aveAngleRadians = averageAngle*pi/180.0
 
283
                    labelRadius = wedgeStyle.labelRadius
 
284
                    labelX = cx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
 
285
                    labelY = cy + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
 
286
                    _addWedgeLabel(self,text,g.add,averageAngle,labelX,labelY,wedgeStyle)
 
287
 
 
288
            startAngle = endAngle
 
289
            i = i + 1
 
290
 
 
291
        return g
 
292
 
 
293
    def draw(self):
 
294
        g = Group()
 
295
        g.add(self.makeWedges())
 
296
        return g
 
297
 
 
298
class LegendedPie(Pie):
 
299
    """Pie with a two part legend (one editable with swatches, one hidden without swatches)."""
 
300
 
 
301
    _attrMap = AttrMap(BASE=Pie,
 
302
        drawLegend = AttrMapValue(isBoolean, desc="If true then create and draw legend"),
 
303
        legend1 = AttrMapValue(None, desc="Handle to legend for pie"),
 
304
        legendNumberFormat = AttrMapValue(None, desc="Formatting routine for number on right hand side of legend."),
 
305
        legendNumberOffset = AttrMapValue(isNumber, desc="Horizontal space between legend and numbers on r/hand side"),
 
306
        pieAndLegend_colors = AttrMapValue(isListOfColors, desc="Colours used for both swatches and pie"),
 
307
        legend_names = AttrMapValue(isNoneOrListOfNoneOrStrings, desc="Names used in legend (or None)"),
 
308
        legend_data = AttrMapValue(isNoneOrListOfNoneOrNumbers, desc="Numbers used on r/hand side of legend (or None)"),
 
309
        leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'),
 
310
        rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'),
 
311
        topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'),
 
312
        bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
 
313
        )
 
314
 
 
315
    def __init__(self):
 
316
        Pie.__init__(self)
 
317
        self.x = 0
 
318
        self.y = 0
 
319
        self.height = 100
 
320
        self.width = 100
 
321
        self.data = [38.4, 20.7, 18.9, 15.4, 6.6]
 
322
        self.labels = None
 
323
        self.direction = 'clockwise'
 
324
        PCMYKColor, black = colors.PCMYKColor, colors.black
 
325
        self.pieAndLegend_colors = [PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV'),
 
326
                                    PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV'),
 
327
                                    PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=75),
 
328
                                    PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=75),
 
329
                                    PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=50),
 
330
                                    PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=50)]
 
331
 
 
332
        #Allows us up to six 'wedges' to be coloured
 
333
        self.slices[0].fillColor=self.pieAndLegend_colors[0]
 
334
        self.slices[1].fillColor=self.pieAndLegend_colors[1]
 
335
        self.slices[2].fillColor=self.pieAndLegend_colors[2]
 
336
        self.slices[3].fillColor=self.pieAndLegend_colors[3]
 
337
        self.slices[4].fillColor=self.pieAndLegend_colors[4]
 
338
        self.slices[5].fillColor=self.pieAndLegend_colors[5]
 
339
 
 
340
        self.slices.strokeWidth = 0.75
 
341
        self.slices.strokeColor = black
 
342
 
 
343
        legendOffset = 17
 
344
        self.legendNumberOffset = 51
 
345
        self.legendNumberFormat = '%.1f%%'
 
346
        self.legend_data = self.data
 
347
 
 
348
        #set up the legends
 
349
        from reportlab.graphics.charts.legends import Legend
 
350
        self.legend1 = Legend()
 
351
        self.legend1.x = self.width+legendOffset
 
352
        self.legend1.y = self.height
 
353
        self.legend1.deltax = 5.67
 
354
        self.legend1.deltay = 14.17
 
355
        self.legend1.dxTextSpace = 11.39
 
356
        self.legend1.dx = 5.67
 
357
        self.legend1.dy = 5.67
 
358
        self.legend1.columnMaximum = 7
 
359
        self.legend1.alignment = 'right'
 
360
        self.legend_names = ['AAA:','AA:','A:','BBB:','NR:']
 
361
        for f in range(0,len(self.data)):
 
362
            self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f]))
 
363
        self.legend1.fontName = "Helvetica-Bold"
 
364
        self.legend1.fontSize = 6
 
365
        self.legend1.strokeColor = black
 
366
        self.legend1.strokeWidth = 0.5
 
367
 
 
368
        self._legend2 = Legend()
 
369
        self._legend2.dxTextSpace = 0
 
370
        self._legend2.dx = 0
 
371
        self._legend2.alignment = 'right'
 
372
        self._legend2.fontName = "Helvetica-Oblique"
 
373
        self._legend2.fontSize = 6
 
374
        self._legend2.strokeColor = self.legend1.strokeColor
 
375
 
 
376
        self.leftPadding = 5
 
377
        self.rightPadding = 5
 
378
        self.topPadding = 5
 
379
        self.bottomPadding = 5
 
380
        self.drawLegend = 1
 
381
 
 
382
    def draw(self):
 
383
        if self.drawLegend:
 
384
            self.legend1.colorNamePairs = []
 
385
            self._legend2.colorNamePairs = []
 
386
        for f in range(0,len(self.data)):
 
387
            if self.legend_names == None:
 
388
                self.slices[f].fillColor = self.pieAndLegend_colors[f]
 
389
                self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], None))
 
390
            else:
 
391
                try:
 
392
                    self.slices[f].fillColor = self.pieAndLegend_colors[f]
 
393
                    self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f]))
 
394
                except IndexError:
 
395
                    self.slices[f].fillColor = self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)]
 
396
                    self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)], self.legend_names[f]))
 
397
            if self.legend_data != None:
 
398
                ldf = self.legend_data[f]
 
399
                lNF = self.legendNumberFormat
 
400
                from types import StringType
 
401
                if ldf is None or lNF is None:
 
402
                    pass
 
403
                elif type(lNF) is StringType:
 
404
                    ldf = lNF % ldf
 
405
                elif callable(lNF):
 
406
                    ldf = lNF(ldf)
 
407
                else:
 
408
                    p = self.legend_names[f]
 
409
                if self.legend_data != None:
 
410
                    ldf = self.legend_data[f]
 
411
                    lNF = self.legendNumberFormat
 
412
                    if ldf is None or lNF is None:
 
413
                        pass
 
414
                    elif type(lNF) is StringType:
 
415
                        ldf = lNF % ldf
 
416
                    elif callable(lNF):
 
417
                        ldf = lNF(ldf)
 
418
                    else:
 
419
                        msg = "Unknown formatter type %s, expected string or function" % self.legendNumberFormat
 
420
                        raise Exception, msg
 
421
                    self._legend2.colorNamePairs.append((None,ldf))
 
422
        p = Pie.draw(self)
 
423
        if self.drawLegend:
 
424
            p.add(self.legend1)
 
425
            #hide from user - keeps both sides lined up!
 
426
            self._legend2.x = self.legend1.x+self.legendNumberOffset
 
427
            self._legend2.y = self.legend1.y
 
428
            self._legend2.deltax = self.legend1.deltax
 
429
            self._legend2.deltay = self.legend1.deltay
 
430
            self._legend2.dy = self.legend1.dy
 
431
            self._legend2.columnMaximum = self.legend1.columnMaximum
 
432
            p.add(self._legend2)
 
433
        p.shift(self.leftPadding, self.bottomPadding)
 
434
        return p
 
435
 
 
436
    def _getDrawingDimensions(self):
 
437
        tx = self.rightPadding
 
438
        if self.drawLegend:
 
439
            tx = tx+self.legend1.x+self.legendNumberOffset #self._legend2.x
 
440
            tx = tx + self._legend2._calculateMaxWidth(self._legend2.colorNamePairs)
 
441
        ty = self.bottomPadding+self.height+self.topPadding
 
442
        return (tx,ty)
 
443
 
 
444
    def demo(self, drawing=None):
 
445
        if not drawing:
 
446
            tx,ty = self._getDrawingDimensions()
 
447
            drawing = Drawing(tx, ty)
 
448
        drawing.add(self.draw())
 
449
        return drawing
 
450
 
 
451
from utils3d import _getShaded, _2rad, _360, _pi_2, _2pi
 
452
class Wedge3dProperties(PropHolder):
 
453
    """This holds descriptive information about the wedges in a pie chart.
 
454
 
 
455
    It is not to be confused with the 'wedge itself'; this just holds
 
456
    a recipe for how to format one, and does not allow you to hack the
 
457
    angles.  It can format a genuine Wedge object for you with its
 
458
    format method.
 
459
    """
 
460
    _attrMap = AttrMap(
 
461
        fillColor = AttrMapValue(isColorOrNone),
 
462
        fillColorShaded = AttrMapValue(isColorOrNone),
 
463
        fontColor = AttrMapValue(isColorOrNone),
 
464
        fontName = AttrMapValue(isString),
 
465
        fontSize = AttrMapValue(isNumber),
 
466
        label_angle = AttrMapValue(isNumber),
 
467
        label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'),
 
468
        label_boxAnchor = AttrMapValue(isBoxAnchor),
 
469
        label_boxFillColor = AttrMapValue(isColorOrNone),
 
470
        label_boxStrokeColor = AttrMapValue(isColorOrNone),
 
471
        label_boxStrokeWidth = AttrMapValue(isNumber),
 
472
        label_dx = AttrMapValue(isNumber),
 
473
        label_dy = AttrMapValue(isNumber),
 
474
        label_height = AttrMapValue(isNumberOrNone),
 
475
        label_leading = AttrMapValue(isNumberOrNone),
 
476
        label_leftPadding = AttrMapValue(isNumber,'padding at left of box'),
 
477
        label_maxWidth = AttrMapValue(isNumberOrNone),
 
478
        label_rightPadding = AttrMapValue(isNumber,'padding at right of box'),
 
479
        label_strokeColor = AttrMapValue(isColorOrNone),
 
480
        label_strokeWidth = AttrMapValue(isNumber),
 
481
        label_text = AttrMapValue(isStringOrNone),
 
482
        label_textAnchor = AttrMapValue(isTextAnchor),
 
483
        label_topPadding = AttrMapValue(isNumber,'padding at top of box'),
 
484
        label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
 
485
        label_width = AttrMapValue(isNumberOrNone),
 
486
        labelRadius = AttrMapValue(isNumber),
 
487
        popout = AttrMapValue(isNumber),
 
488
        shading = AttrMapValue(isNumber),
 
489
        strokeColor = AttrMapValue(isColorOrNone),
 
490
        strokeColorShaded = AttrMapValue(isColorOrNone),
 
491
        strokeDashArray = AttrMapValue(isListOfNumbersOrNone),
 
492
        strokeWidth = AttrMapValue(isNumber),
 
493
        visible = AttrMapValue(isBoolean,'set to false to skip displaying'),
 
494
        )
 
495
 
 
496
    def __init__(self):
 
497
        self.strokeWidth = 0
 
498
        self.shading = 0.3
 
499
        self.visible = 1
 
500
        self.strokeColorShaded = self.fillColorShaded = self.fillColor = None
 
501
        self.strokeColor = STATE_DEFAULTS["strokeColor"]
 
502
        self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
 
503
        self.popout = 0
 
504
        self.fontName = STATE_DEFAULTS["fontName"]
 
505
        self.fontSize = STATE_DEFAULTS["fontSize"]
 
506
        self.fontColor = STATE_DEFAULTS["fillColor"]
 
507
        self.labelRadius = 1.2
 
508
        self.label_dx = self.label_dy = self.label_angle = 0
 
509
        self.label_text = None
 
510
        self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0
 
511
        self.label_boxAnchor = 'c'
 
512
        self.label_boxStrokeColor = None    #boxStroke
 
513
        self.label_boxStrokeWidth = 0.5 #boxStrokeWidth
 
514
        self.label_boxFillColor = None
 
515
        self.label_strokeColor = None
 
516
        self.label_strokeWidth = 0.1
 
517
        self.label_leading =    self.label_width = self.label_maxWidth = self.label_height = None
 
518
        self.label_textAnchor = 'start'
 
519
        self.label_visible = 1
 
520
 
 
521
class _SL3D:
 
522
    def __init__(self,lo,hi):
 
523
        if lo<0:
 
524
            lo += 360
 
525
            hi += 360
 
526
        self.lo = lo
 
527
        self.hi = hi
 
528
        self.mid = (lo+hi)*0.5
 
529
 
 
530
    def __str__(self):
 
531
        return '_SL3D(%.2f,%.2f)' % (self.lo,self.hi)
 
532
 
 
533
_270r = _2rad(270)
 
534
class Pie3d(Pie):
 
535
    _attrMap = AttrMap(BASE=Pie,
 
536
        perspective = AttrMapValue(isNumber, desc='A flattening parameter.'),
 
537
        depth_3d = AttrMapValue(isNumber, desc='depth of the pie.'),
 
538
        angle_3d = AttrMapValue(isNumber, desc='The view angle.'),
 
539
        )
 
540
    perspective = 70
 
541
    depth_3d = 25
 
542
    angle_3d = 180
 
543
 
 
544
    def _popout(self,i):
 
545
        return self.slices[i].popout or 0
 
546
 
 
547
    def CX(self, i,d ):
 
548
        return self._cx+(d and self._xdepth_3d or 0)+self._popout(i)*cos(_2rad(self._sl3d[i].mid))
 
549
    def CY(self,i,d):
 
550
        return self._cy+(d and self._ydepth_3d or 0)+self._popout(i)*sin(_2rad(self._sl3d[i].mid))
 
551
    def OX(self,i,o,d):
 
552
        return self.CX(i,d)+self._radiusx*cos(_2rad(o))
 
553
    def OY(self,i,o,d):
 
554
        return self.CY(i,d)+self._radiusy*sin(_2rad(o))
 
555
 
 
556
    def rad_dist(self,a):
 
557
        _3dva = self._3dva
 
558
        return min(abs(a-_3dva),abs(a-_3dva+360))
 
559
 
 
560
    def __init__(self):
 
561
        self.x = 0
 
562
        self.y = 0
 
563
        self.width = 300
 
564
        self.height = 200
 
565
        self.data = [12.50,20.10,2.00,22.00,5.00,18.00,13.00]
 
566
        self.labels = None  # or list of strings
 
567
        self.startAngle = 90
 
568
        self.direction = "clockwise"
 
569
        self.simpleLabels = 1
 
570
        self.slices = TypedPropertyCollection(Wedge3dProperties)
 
571
        self.slices[0].fillColor = colors.darkcyan
 
572
        self.slices[1].fillColor = colors.blueviolet
 
573
        self.slices[2].fillColor = colors.blue
 
574
        self.slices[3].fillColor = colors.cyan
 
575
        self.slices[4].fillColor = colors.azure
 
576
        self.slices[5].fillColor = colors.crimson
 
577
        self.slices[6].fillColor = colors.darkviolet
 
578
 
 
579
    def _fillSide(self,L,i,angle,strokeColor,strokeWidth,fillColor):
 
580
        rd = self.rad_dist(angle)
 
581
        if rd<self.rad_dist(self._sl3d[i].mid):
 
582
            p = [self.CX(i,0),self.CY(i,0),
 
583
                self.CX(i,1),self.CY(i,1),
 
584
                self.OX(i,angle,1),self.OY(i,angle,1),
 
585
                self.OX(i,angle,0),self.OY(i,angle,0)]
 
586
            L.append((rd,Polygon(p, strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1)))
 
587
 
 
588
    def draw(self):
 
589
        slices = self.slices
 
590
        _3d_angle = self.angle_3d
 
591
        _3dva = self._3dva = _360(_3d_angle+90)
 
592
        a0 = _2rad(_3dva)
 
593
        self._xdepth_3d = cos(a0)*self.depth_3d
 
594
        self._ydepth_3d = sin(a0)*self.depth_3d
 
595
        self._cx = self.x+self.width/2.0
 
596
        self._cy = self.y+(self.height - self._ydepth_3d)/2.0
 
597
        radius = self._radius = self._cx-self.x
 
598
        self._radiusx = radiusx = radius
 
599
        self._radiusy = radiusy = (1.0 - self.perspective/100.0)*radius
 
600
        data = self.normalizeData()
 
601
        sum = self._sum
 
602
 
 
603
        CX = self.CX
 
604
        CY = self.CY
 
605
        OX = self.OX
 
606
        OY = self.OY
 
607
        rad_dist = self.rad_dist
 
608
        _fillSide = self._fillSide
 
609
        n = len(data)
 
610
        _sl3d = self._sl3d = []
 
611
        g = Group()
 
612
        last = _360(self.startAngle)
 
613
        a0 = self.direction=='clockwise' and -1 or 1
 
614
        for v in data:
 
615
            v *= a0
 
616
            angle1, angle0 = last, v+last
 
617
            last = angle0
 
618
            if a0>0: angle0, angle1 = angle1, angle0
 
619
            _sl3d.append(_SL3D(angle0,angle1))
 
620
            #print '%d: %.2f %.2f --> %s' %(len(_sl3d)-1,angle0,angle1,_sl3d[-1])
 
621
 
 
622
        labels = _fixLabels(self.labels,n)
 
623
        a0 = _3d_angle
 
624
        a1 = _3d_angle+180
 
625
        T = []
 
626
        S = []
 
627
        L = []
 
628
 
 
629
        class WedgeLabel3d(WedgeLabel):
 
630
            def _checkDXY(self,ba):
 
631
                if ba[0]=='n':
 
632
                    if not hasattr(self,'_ody'):
 
633
                        self._ody = self.dy
 
634
                        self.dy = -self._ody + self._ydepth_3d
 
635
        WedgeLabel3d._ydepth_3d = self._ydepth_3d
 
636
 
 
637
        for i in xrange(n):
 
638
            style = slices[i]
 
639
            if not style.visible: continue
 
640
            sl = _sl3d[i]
 
641
            lo = angle0 = sl.lo
 
642
            hi = angle1 = sl.hi
 
643
            if abs(hi-lo)<=1e-7: continue
 
644
            fillColor = _getShaded(style.fillColor,style.fillColorShaded,style.shading)
 
645
            strokeColor = _getShaded(style.strokeColor,style.strokeColorShaded,style.shading) or fillColor
 
646
            strokeWidth = style.strokeWidth
 
647
            cx0 = CX(i,0)
 
648
            cy0 = CY(i,0)
 
649
            cx1 = CX(i,1)
 
650
            cy1 = CY(i,1)
 
651
            #background shaded pie bottom
 
652
            g.add(Wedge(cx1,cy1,radiusx, lo, hi,yradius=radiusy,
 
653
                            strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,
 
654
                            strokeLineJoin=1))
 
655
            #connect to top
 
656
            if lo < a0 < hi: angle0 = a0
 
657
            if lo < a1 < hi: angle1 = a1
 
658
            if 1:
 
659
                p = ArcPath(strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1)
 
660
                p.addArc(cx1,cy1,radiusx,angle0,angle1,yradius=radiusy,moveTo=1)
 
661
                p.lineTo(OX(i,angle1,0),OY(i,angle1,0))
 
662
                p.addArc(cx0,cy0,radiusx,angle0,angle1,yradius=radiusy,reverse=1)
 
663
                p.closePath()
 
664
                if angle0<=_3dva and angle1>=_3dva:
 
665
                    rd = 0
 
666
                else:
 
667
                    rd = min(rad_dist(angle0),rad_dist(angle1))
 
668
                S.append((rd,p))
 
669
            _fillSide(S,i,lo,strokeColor,strokeWidth,fillColor)
 
670
            _fillSide(S,i,hi,strokeColor,strokeWidth,fillColor)
 
671
 
 
672
            #bright shaded top
 
673
            fillColor = style.fillColor
 
674
            strokeColor = style.strokeColor or fillColor
 
675
            T.append(Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy,
 
676
                            strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1))
 
677
 
 
678
            text = labels[i]
 
679
            if text:
 
680
                rat = style.labelRadius
 
681
                self._radiusx *= rat
 
682
                self._radiusy *= rat
 
683
                mid = sl.mid
 
684
                _addWedgeLabel(self,text,L.append,mid,OX(i,mid,0),OY(i,mid,0),style,labelClass=WedgeLabel3d)
 
685
                self._radiusx = radiusx
 
686
                self._radiusy = radiusy
 
687
 
 
688
        S.sort(lambda a,b: -cmp(a[0],b[0]))
 
689
        map(g.add,map(lambda x:x[1],S)+T+L)
 
690
        return g
 
691
 
 
692
    def demo(self):
 
693
        d = Drawing(200, 100)
 
694
 
 
695
        pc = Pie()
 
696
        pc.x = 50
 
697
        pc.y = 10
 
698
        pc.width = 100
 
699
        pc.height = 80
 
700
        pc.data = [10,20,30,40,50,60]
 
701
        pc.labels = ['a','b','c','d','e','f']
 
702
 
 
703
        pc.slices.strokeWidth=0.5
 
704
        pc.slices[3].popout = 10
 
705
        pc.slices[3].strokeWidth = 2
 
706
        pc.slices[3].strokeDashArray = [2,2]
 
707
        pc.slices[3].labelRadius = 1.75
 
708
        pc.slices[3].fontColor = colors.red
 
709
        pc.slices[0].fillColor = colors.darkcyan
 
710
        pc.slices[1].fillColor = colors.blueviolet
 
711
        pc.slices[2].fillColor = colors.blue
 
712
        pc.slices[3].fillColor = colors.cyan
 
713
        pc.slices[4].fillColor = colors.aquamarine
 
714
        pc.slices[5].fillColor = colors.cadetblue
 
715
        pc.slices[6].fillColor = colors.lightcoral
 
716
        self.slices[1].visible = 0
 
717
        self.slices[3].visible = 1
 
718
        self.slices[4].visible = 1
 
719
        self.slices[5].visible = 1
 
720
        self.slices[6].visible = 0
 
721
 
 
722
        d.add(pc)
 
723
        return d
 
724
 
 
725
 
 
726
def sample0a():
 
727
    "Make a degenerated pie chart with only one slice."
 
728
 
 
729
    d = Drawing(400, 200)
 
730
 
 
731
    pc = Pie()
 
732
    pc.x = 150
 
733
    pc.y = 50
 
734
    pc.data = [10]
 
735
    pc.labels = ['a']
 
736
    pc.slices.strokeWidth=1#0.5
 
737
 
 
738
    d.add(pc)
 
739
 
 
740
    return d
 
741
 
 
742
 
 
743
def sample0b():
 
744
    "Make a degenerated pie chart with only one slice."
 
745
 
 
746
    d = Drawing(400, 200)
 
747
 
 
748
    pc = Pie()
 
749
    pc.x = 150
 
750
    pc.y = 50
 
751
    pc.width = 120
 
752
    pc.height = 100
 
753
    pc.data = [10]
 
754
    pc.labels = ['a']
 
755
    pc.slices.strokeWidth=1#0.5
 
756
 
 
757
    d.add(pc)
 
758
 
 
759
    return d
 
760
 
 
761
 
 
762
def sample1():
 
763
    "Make a typical pie chart with with one slice treated in a special way."
 
764
 
 
765
    d = Drawing(400, 200)
 
766
 
 
767
    pc = Pie()
 
768
    pc.x = 150
 
769
    pc.y = 50
 
770
    pc.data = [10, 20, 30, 40, 50, 60]
 
771
    pc.labels = ['a', 'b', 'c', 'd', 'e', 'f']
 
772
 
 
773
    pc.slices.strokeWidth=1#0.5
 
774
    pc.slices[3].popout = 20
 
775
    pc.slices[3].strokeWidth = 2
 
776
    pc.slices[3].strokeDashArray = [2,2]
 
777
    pc.slices[3].labelRadius = 1.75
 
778
    pc.slices[3].fontColor = colors.red
 
779
 
 
780
    d.add(pc)
 
781
 
 
782
    return d
 
783
 
 
784
 
 
785
def sample2():
 
786
    "Make a pie chart with nine slices."
 
787
 
 
788
    d = Drawing(400, 200)
 
789
 
 
790
    pc = Pie()
 
791
    pc.x = 125
 
792
    pc.y = 25
 
793
    pc.data = [0.31, 0.148, 0.108,
 
794
               0.076, 0.033, 0.03,
 
795
               0.019, 0.126, 0.15]
 
796
    pc.labels = ['1', '2', '3', '4', '5', '6', '7', '8', 'X']
 
797
 
 
798
    pc.width = 150
 
799
    pc.height = 150
 
800
    pc.slices.strokeWidth=1#0.5
 
801
 
 
802
    pc.slices[0].fillColor = colors.steelblue
 
803
    pc.slices[1].fillColor = colors.thistle
 
804
    pc.slices[2].fillColor = colors.cornflower
 
805
    pc.slices[3].fillColor = colors.lightsteelblue
 
806
    pc.slices[4].fillColor = colors.aquamarine
 
807
    pc.slices[5].fillColor = colors.cadetblue
 
808
    pc.slices[6].fillColor = colors.lightcoral
 
809
    pc.slices[7].fillColor = colors.tan
 
810
    pc.slices[8].fillColor = colors.darkseagreen
 
811
 
 
812
    d.add(pc)
 
813
 
 
814
    return d
 
815
 
 
816
 
 
817
def sample3():
 
818
    "Make a pie chart with a very slim slice."
 
819
 
 
820
    d = Drawing(400, 200)
 
821
 
 
822
    pc = Pie()
 
823
    pc.x = 125
 
824
    pc.y = 25
 
825
 
 
826
    pc.data = [74, 1, 25]
 
827
 
 
828
    pc.width = 150
 
829
    pc.height = 150
 
830
    pc.slices.strokeWidth=1#0.5
 
831
    pc.slices[0].fillColor = colors.steelblue
 
832
    pc.slices[1].fillColor = colors.thistle
 
833
    pc.slices[2].fillColor = colors.cornflower
 
834
 
 
835
    d.add(pc)
 
836
 
 
837
    return d
 
838
 
 
839
 
 
840
def sample4():
 
841
    "Make a pie chart with several very slim slices."
 
842
 
 
843
    d = Drawing(400, 200)
 
844
 
 
845
    pc = Pie()
 
846
    pc.x = 125
 
847
    pc.y = 25
 
848
 
 
849
    pc.data = [74, 1, 1, 1, 1, 22]
 
850
 
 
851
    pc.width = 150
 
852
    pc.height = 150
 
853
    pc.slices.strokeWidth=1#0.5
 
854
    pc.slices[0].fillColor = colors.steelblue
 
855
    pc.slices[1].fillColor = colors.thistle
 
856
    pc.slices[2].fillColor = colors.cornflower
 
857
    pc.slices[3].fillColor = colors.lightsteelblue
 
858
    pc.slices[4].fillColor = colors.aquamarine
 
859
    pc.slices[5].fillColor = colors.cadetblue
 
860
 
 
861
    d.add(pc)
 
862
 
 
863
    return d