1#Copyright ReportLab Europe Ltd. 2000-2017 2#see license.txt for license details 3#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/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__version__='3.3.0' 10__doc__="""Basic Pie Chart class. 11 12This permits you to customize and pop out individual wedges; 13supports elliptical and circular pies. 14""" 15 16import copy, functools 17from math import sin, cos, pi 18 19from reportlab.lib import colors 20from 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, NoneOr, EitherOr,\ 27 isNumberInRange 28from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol 29from reportlab.lib.attrmap import * 30from reportlab.pdfgen.canvas import Canvas 31from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, ArcPath, Polygon, Rect, PolyLine, Line 32from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder 33from reportlab.graphics.charts.areas import PlotArea 34from reportlab.graphics.charts.legends import _objStr 35from reportlab.graphics.charts.textlabels import Label 36from reportlab import xrange, ascii, cmp 37 38_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'} 39_ANGLE2RBOXANCHOR={0:'e', 45:'ne', 90:'n', 135:'nw', 180:'w', 225:'sw', 270:'s', 315: 'se', -45: 'se'} 40 41_ANGLELO = 1e-7 42_ANGLEHI = 360.0 - _ANGLELO 43 44class WedgeLabel(Label): 45 def _checkDXY(self,ba): 46 pass 47 def _getBoxAnchor(self): 48 ba = self.boxAnchor 49 if ba in ('autox','autoy'): 50 na = (int((self._pmv%360)/45.)*45)%360 51 if not (na % 90): # we have a right angle case 52 da = (self._pmv - na) % 360 53 if abs(da)>5: 54 na += (da>0 and 45 or -45) 55 ba = (getattr(self,'_anti',None) and _ANGLE2RBOXANCHOR or _ANGLE2BOXANCHOR)[na] 56 self._checkDXY(ba) 57 return ba 58 59class WedgeProperties(PropHolder): 60 """This holds descriptive information about the wedges in a pie chart. 61 62 It is not to be confused with the 'wedge itself'; this just holds 63 a recipe for how to format one, and does not allow you to hack the 64 angles. It can format a genuine Wedge object for you with its 65 format method. 66 """ 67 _attrMap = AttrMap( 68 strokeWidth = AttrMapValue(isNumber,desc='Width of the wedge border'), 69 fillColor = AttrMapValue(isColorOrNone,desc='Filling color of the wedge'), 70 strokeColor = AttrMapValue(isColorOrNone,desc='Color of the wedge border'), 71 strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='Style of the wedge border, expressed as a list of lengths of alternating dashes and blanks'), 72 strokeLineCap = AttrMapValue(OneOf(0,1,2),desc="Line cap 0=butt, 1=round & 2=square"), 73 strokeLineJoin = AttrMapValue(OneOf(0,1,2),desc="Line join 0=miter, 1=round & 2=bevel"), 74 strokeMiterLimit = AttrMapValue(isNumber,desc='Miter limit control miter line joins'), 75 popout = AttrMapValue(isNumber,desc="How far of centre a wedge to pop"), 76 fontName = AttrMapValue(isString,desc='Name of the font of the label text'), 77 fontSize = AttrMapValue(isNumber,desc='Size of the font of the label text in points'), 78 fontColor = AttrMapValue(isColorOrNone,desc='Color of the font of the label text'), 79 labelRadius = AttrMapValue(isNumber,desc='Distance between the center of the label box and the center of the pie, expressed in times the radius of the pie'), 80 label_dx = AttrMapValue(isNumber,desc='X Offset of the label'), 81 label_dy = AttrMapValue(isNumber,desc='Y Offset of the label'), 82 label_angle = AttrMapValue(isNumber,desc='Angle of the label, default (0) is horizontal, 90 is vertical, 180 is upside down'), 83 label_boxAnchor = AttrMapValue(isBoxAnchor,desc='Anchoring point of the label'), 84 label_boxStrokeColor = AttrMapValue(isColorOrNone,desc='Border color for the label box'), 85 label_boxStrokeWidth = AttrMapValue(isNumber,desc='Border width for the label box'), 86 label_boxFillColor = AttrMapValue(isColorOrNone,desc='Filling color of the label box'), 87 label_strokeColor = AttrMapValue(isColorOrNone,desc='Border color for the label text'), 88 label_strokeWidth = AttrMapValue(isNumber,desc='Border width for the label text'), 89 label_text = AttrMapValue(isStringOrNone,desc='Text of the label'), 90 label_leading = AttrMapValue(isNumberOrNone,desc=''), 91 label_width = AttrMapValue(isNumberOrNone,desc='Width of the label'), 92 label_maxWidth = AttrMapValue(isNumberOrNone,desc='Maximum width the label can grow to'), 93 label_height = AttrMapValue(isNumberOrNone,desc='Height of the label'), 94 label_textAnchor = AttrMapValue(isTextAnchor,desc='Maximum height the label can grow to'), 95 label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), 96 label_topPadding = AttrMapValue(isNumber,'Padding at top of box'), 97 label_leftPadding = AttrMapValue(isNumber,'Padding at left of box'), 98 label_rightPadding = AttrMapValue(isNumber,'Padding at right of box'), 99 label_bottomPadding = AttrMapValue(isNumber,'Padding at bottom of box'), 100 label_simple_pointer = AttrMapValue(isBoolean,'Set to True for simple pointers'), 101 label_pointer_strokeColor = AttrMapValue(isColorOrNone,desc='Color of indicator line'), 102 label_pointer_strokeWidth = AttrMapValue(isNumber,desc='StrokeWidth of indicator line'), 103 label_pointer_elbowLength = AttrMapValue(isNumber,desc='Length of final indicator line segment'), 104 label_pointer_edgePad = AttrMapValue(isNumber,desc='pad between pointer label and box'), 105 label_pointer_piePad = AttrMapValue(isNumber,desc='pad between pointer label and pie'), 106 swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ...",advancedUsage=1), 107 visible = AttrMapValue(isBoolean,'Set to false to skip displaying'), 108 shadingAmount = AttrMapValue(isNumberOrNone,desc='amount by which to shade fillColor'), 109 shadingAngle = AttrMapValue(isNumber,desc='shading changes at multiple of this angle (in degrees)'), 110 shadingDirection = AttrMapValue(OneOf('normal','anti'),desc="Whether shading is at start or end of wedge/sector"), 111 shadingKind = AttrMapValue(OneOf(None,'lighten','darken'),desc="use colors.Whiter or Blacker"), 112 ) 113 114 def __init__(self): 115 self.strokeWidth = 0 116 self.fillColor = None 117 self.strokeColor = STATE_DEFAULTS["strokeColor"] 118 self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] 119 self.strokeLineJoin = 1 120 self.strokeLineCap = 0 121 self.strokeMiterLimit = 0 122 self.popout = 0 123 self.fontName = STATE_DEFAULTS["fontName"] 124 self.fontSize = STATE_DEFAULTS["fontSize"] 125 self.fontColor = STATE_DEFAULTS["fillColor"] 126 self.labelRadius = 1.2 127 self.label_dx = self.label_dy = self.label_angle = 0 128 self.label_text = None 129 self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 130 self.label_boxAnchor = 'autox' 131 self.label_boxStrokeColor = None #boxStroke 132 self.label_boxStrokeWidth = 0.5 #boxStrokeWidth 133 self.label_boxFillColor = None 134 self.label_strokeColor = None 135 self.label_strokeWidth = 0.1 136 self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None 137 self.label_textAnchor = 'start' 138 self.label_simple_pointer = 0 139 self.label_visible = 1 140 self.label_pointer_strokeColor = colors.black 141 self.label_pointer_strokeWidth = 0.5 142 self.label_pointer_elbowLength = 3 143 self.label_pointer_edgePad = 2 144 self.label_pointer_piePad = 3 145 self.visible = 1 146 self.shadingKind = None 147 self.shadingAmount = 0.5 148 self.shadingAngle = 2.0137 149 self.shadingDirection = 'normal' #or 'anti' 150 151def _addWedgeLabel(self,text,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel): 152 # now draw a label 153 if self.simpleLabels: 154 theLabel = String(labelX, labelY, text) 155 if not self.sideLabels: 156 theLabel.textAnchor = "middle" 157 else: 158 if (abs(angle) < 90 ) or (angle >270 and angle<450) or (-450< angle <-270): 159 theLabel.textAnchor = "start" 160 else: 161 theLabel.textAnchor = "end" 162 theLabel._pmv = angle 163 theLabel._simple_pointer = 0 164 else: 165 theLabel = labelClass() 166 theLabel._pmv = angle 167 theLabel.x = labelX 168 theLabel.y = labelY 169 theLabel.dx = wedgeStyle.label_dx 170 if not self.sideLabels: 171 theLabel.dy = wedgeStyle.label_dy 172 theLabel.boxAnchor = wedgeStyle.label_boxAnchor 173 else: 174 if wedgeStyle.fontSize is None: 175 sideLabels_dy = self.fontSize / 2.5 176 else: 177 sideLabels_dy = wedgeStyle.fontSize / 2.5 178 if wedgeStyle.label_dy is None: 179 theLabel.dy = sideLabels_dy 180 else: 181 theLabel.dy = wedgeStyle.label_dy + sideLabels_dy 182 if (abs(angle) < 90 ) or (angle >270 and angle<450) or (-450< angle <-270): 183 theLabel.boxAnchor = 'w' 184 else: 185 theLabel.boxAnchor = 'e' 186 theLabel.angle = wedgeStyle.label_angle 187 theLabel.boxStrokeColor = wedgeStyle.label_boxStrokeColor 188 theLabel.boxStrokeWidth = wedgeStyle.label_boxStrokeWidth 189 theLabel.boxFillColor = wedgeStyle.label_boxFillColor 190 theLabel.strokeColor = wedgeStyle.label_strokeColor 191 theLabel.strokeWidth = wedgeStyle.label_strokeWidth 192 _text = wedgeStyle.label_text 193 if _text is None: _text = text 194 theLabel._text = _text 195 theLabel.leading = wedgeStyle.label_leading 196 theLabel.width = wedgeStyle.label_width 197 theLabel.maxWidth = wedgeStyle.label_maxWidth 198 theLabel.height = wedgeStyle.label_height 199 theLabel.textAnchor = wedgeStyle.label_textAnchor 200 theLabel.visible = wedgeStyle.label_visible 201 theLabel.topPadding = wedgeStyle.label_topPadding 202 theLabel.leftPadding = wedgeStyle.label_leftPadding 203 theLabel.rightPadding = wedgeStyle.label_rightPadding 204 theLabel.bottomPadding = wedgeStyle.label_bottomPadding 205 theLabel._simple_pointer = wedgeStyle.label_simple_pointer 206 theLabel.fontSize = wedgeStyle.fontSize 207 theLabel.fontName = wedgeStyle.fontName 208 theLabel.fillColor = wedgeStyle.fontColor 209 return theLabel 210 211def _fixLabels(labels,n): 212 if labels is None: 213 labels = [''] * n 214 else: 215 i = n-len(labels) 216 if i>0: labels = list(labels)+['']*i 217 return labels 218 219class AbstractPieChart(PlotArea): 220 221 def makeSwatchSample(self, rowNo, x, y, width, height): 222 baseStyle = self.slices 223 styleIdx = rowNo % len(baseStyle) 224 style = baseStyle[styleIdx] 225 strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) 226 fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) 227 strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) 228 strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',None)) 229 swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) 230 if swatchMarker: 231 return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) 232 return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, 233 strokeDashArray=strokeDashArray,fillColor=fillColor) 234 235 def getSeriesName(self,i,default=None): 236 '''return series name i or default''' 237 try: 238 text = _objStr(self.labels[i]) 239 except: 240 text = default 241 if not self.simpleLabels: 242 _text = getattr(self.slices[i],'label_text','') 243 if _text is not None: text = _text 244 return text 245 246def boundsOverlap(P,Q): 247 return not(P[0]>Q[2]-1e-2 or Q[0]>P[2]-1e-2 or P[1]>(0.5*(Q[1]+Q[3]))-1e-2 or Q[1]>(0.5*(P[1]+P[3]))-1e-2) 248 249def _findOverlapRun(B,i,wrap): 250 '''find overlap run containing B[i]''' 251 n = len(B) 252 R = [i] 253 while 1: 254 i = R[-1] 255 j = (i+1)%n 256 if j in R or not boundsOverlap(B[i],B[j]): break 257 R.append(j) 258 while 1: 259 i = R[0] 260 j = (i-1)%n 261 if j in R or not boundsOverlap(B[i],B[j]): break 262 R.insert(0,j) 263 return R 264 265def findOverlapRun(B,wrap=1): 266 '''determine a set of overlaps in bounding boxes B or return None''' 267 n = len(B) 268 if n>1: 269 for i in xrange(n-1): 270 R = _findOverlapRun(B,i,wrap) 271 if len(R)>1: return R 272 return None 273 274def fixLabelOverlaps(L, sideLabels=False, mult0=1.0): 275 nL = len(L) 276 if nL<2: return 277 B = [l._origdata['bounds'] for l in L] 278 OK = 1 279 RP = [] 280 iter = 0 281 mult0 = float(mult0 + 0) 282 mult = mult0 283 284 if not sideLabels: 285 while iter<30: 286 R = findOverlapRun(B) 287 if not R: break 288 nR = len(R) 289 if nR==nL: break 290 if not [r for r in RP if r in R]: 291 mult = mult0 292 da = 0 293 r0 = R[0] 294 rL = R[-1] 295 bi = B[r0] 296 taa = aa = _360(L[r0]._pmv) 297 for r in R[1:]: 298 b = B[r] 299 da = max(da,min(b[2]-bi[0],bi[2]-b[0])) 300 bi = b 301 aa += L[r]._pmv 302 aa = aa/float(nR) 303 utaa = abs(L[rL]._pmv-taa) 304 ntaa = _360(utaa) 305 da *= mult*(nR-1)/ntaa 306 307 for r in R: 308 l = L[r] 309 orig = l._origdata 310 angle = l._pmv = _360(l._pmv+da*(_360(l._pmv)-aa)) 311 rad = angle/_180_pi 312 l.x = orig['cx'] + orig['rx']*cos(rad) 313 l.y = orig['cy'] + orig['ry']*sin(rad) 314 B[r] = l.getBounds() 315 RP = R 316 mult *= 1.05 317 iter += 1 318 319 else: 320 while iter<30: 321 R = findOverlapRun(B) 322 if not R: break 323 nR = len(R) 324 if nR == nL: break 325 l1 = L[-1] 326 orig1 = l1._origdata 327 bounds1 = orig1['bounds'] 328 for i,r in enumerate(R): 329 l = L[r] 330 orig = l._origdata 331 bounds = orig['bounds'] 332 diff1 = 0 333 diff2 = 0 334 if not i == nR-1: 335 if not bounds == bounds1: 336 if bounds[3]>bounds1[1] and bounds1[1]<bounds[1]: 337 diff1 = bounds[3]-bounds1[1] 338 if bounds1[3]>bounds[1] and bounds[1]<bounds1[1]: 339 diff2 = bounds1[3]-bounds[1] 340 if diff1 > diff2: 341 l.y +=0.5*(bounds1[3]-bounds1[1]) 342 elif diff2 >= diff1: 343 l.y -= 0.5*(bounds1[3]-bounds1[1]) 344 B[r] = l.getBounds() 345 iter += 1 346 347def intervalIntersection(A,B): 348 x,y = max(min(A),min(B)),min(max(A),max(B)) 349 if x>=y: return None 350 return x,y 351 352def _makeSideArcDefs(sa,direction): 353 sa %= 360 354 if 90<=sa<270: 355 if direction=='clockwise': 356 a = (0,90,sa),(1,-90,90),(0,-360+sa,-90) 357 else: 358 a = (0,sa,270),(1,270,450),(0,450,360+sa) 359 else: 360 offs = sa>=270 and 360 or 0 361 if direction=='clockwise': 362 a = (1,offs-90,sa),(0,offs-270,offs-90),(1,-360+sa,offs-270) 363 else: 364 a = (1,sa,offs+90),(0,offs+90,offs+270),(1,offs+270,360+sa) 365 return tuple([a for a in a if a[1]<a[2]]) 366 367def _keyFLA(x,y): 368 return cmp(y[1]-y[0],x[1]-x[0]) 369_keyFLA = functools.cmp_to_key(_keyFLA) 370 371def _findLargestArc(xArcs,side): 372 a = [a[1] for a in xArcs if a[0]==side and a[1] is not None] 373 if not a: return None 374 if len(a)>1: a.sort(key=_keyFLA) 375 return a[0] 376 377def _fPLSide(l,width,side=None): 378 data = l._origdata 379 if side is None: 380 li = data['li'] 381 ri = data['ri'] 382 if li is None: 383 side = 1 384 i = ri 385 elif ri is None: 386 side = 0 387 i = li 388 elif li[1]-li[0]>ri[1]-ri[0]: 389 side = 0 390 i = li 391 else: 392 side = 1 393 i = ri 394 w = data['width'] 395 edgePad = data['edgePad'] 396 if not side: #on left 397 l._pmv = 180 398 l.x = edgePad+w 399 i = data['li'] 400 else: 401 l._pmv = 0 402 l.x = width - w - edgePad 403 i = data['ri'] 404 mid = data['mid'] = (i[0]+i[1])*0.5 405 data['smid'] = sin(mid/_180_pi) 406 data['cmid'] = cos(mid/_180_pi) 407 data['side'] = side 408 return side,w 409 410#key functions 411def _fPLCF(a,b): 412 return cmp(b._origdata['smid'],a._origdata['smid']) 413_fPLCF = functools.cmp_to_key(_fPLCF) 414 415def _arcCF(a): 416 return a[1] 417 418def _fixPointerLabels(n,L,x,y,width,height,side=None): 419 LR = [],[] 420 mlr = [0,0] 421 for l in L: 422 i,w = _fPLSide(l,width,side) 423 LR[i].append(l) 424 mlr[i] = max(w,mlr[i]) 425 mul = 1 426 G = n*[None] 427 mel = 0 428 hh = height*0.5 429 yhh = y+hh 430 m = max(mlr) 431 for i in (0,1): 432 T = LR[i] 433 if T: 434 B = [] 435 aB = B.append 436 S = [] 437 aS = S.append 438 T.sort(key=_fPLCF) 439 p = 0 440 yh = y+height 441 for l in T: 442 data = l._origdata 443 inc = x+mul*(m-data['width']) 444 l.x += inc 445 G[data['index']] = l 446 ly = yhh+data['smid']*hh 447 b = data['bounds'] 448 b2 = (b[3]-b[1])*0.5 449 if ly+b2>yh: ly = yh-b2 450 if ly-b2<y: ly = y+b2 451 data['bounds'] = b = (b[0],ly-b2,b[2],ly+b2) 452 aB(b) 453 l.y = ly 454 aS(max(0,yh-ly-b2)) 455 yh = ly-b2 456 p = max(p,data['edgePad']+data['piePad']) 457 mel = max(mel,abs(data['smid']*(hh+data['elbowLength']))-hh) 458 aS(yh-y) 459 460 iter = 0 461 nT = len(T) 462 while iter<30: 463 R = findOverlapRun(B,wrap=0) 464 if not R: break 465 nR = len(R) 466 if nR==nT: break 467 j0 = R[0] 468 j1 = R[-1] 469 jl = j1+1 470 sAbove = sum(S[:j0+1]) 471 sFree = sAbove+sum(S[jl:]) 472 sNeed = sum([b[3]-b[1] for b in B[j0:jl]])+jl-j0-(B[j0][3]-B[j1][1]) 473 if sNeed>sFree: break 474 yh = B[j0][3]+sAbove*sNeed/sFree 475 for r in R: 476 l = T[r] 477 data = l._origdata 478 b = data['bounds'] 479 b2 = (b[3]-b[1])*0.5 480 yh -= 0.5 481 ly = l.y = yh-b2 482 B[r] = data['bounds'] = (b[0],ly-b2,b[2],yh) 483 yh = ly - b2 - 0.5 484 mlr[i] = m+p 485 mul = -1 486 return G, mlr[0], mlr[1], mel 487 488def theta0(data, direction): 489 fac = (2*pi)/sum(data) 490 rads = [d*fac for d in data] 491 492 r0 = 0 493 hrads = [] 494 for r in rads: 495 hrads.append(r0+r*0.5) 496 r0 += r 497 498 vstar = len(data)*1e6 499 rstar = 0 500 delta = pi/36.0 501 for i in range(36): 502 r = i*delta 503 v = sum([abs(sin(r+a)) for a in hrads]) 504 if v < vstar: 505 if direction == 'clockwise': 506 rstar=-r 507 else: 508 rstar=r 509 vstar = v 510 return rstar*180/pi 511 512 513class AngleData(float): 514 '''use this to carry the data along with the angle''' 515 def __new__(cls,angle,data): 516 self = float.__new__(cls,angle) 517 self._data = data 518 return self 519 520class Pie(AbstractPieChart): 521 _attrMap = AttrMap(BASE=AbstractPieChart, 522 data = AttrMapValue(isListOfNumbers, desc='List of numbers defining wedge sizes; need not sum to 1'), 523 labels = AttrMapValue(isListOfStringsOrNone, desc="Optional list of labels to use for each data point"), 524 startAngle = AttrMapValue(isNumber, desc="Angle of first slice; 0 is due East"), 525 direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), 526 slices = AttrMapValue(None, desc="Collection of wedge descriptor objects"), 527 simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use a simple String not an advanced WedgeLabel. A WedgeLabel is customisable using the properties prefixed label_ in the collection slices."), 528 other_threshold = AttrMapValue(isNumber, desc='A value for doing threshholding, not used yet.',advancedUsage=1), 529 checkLabelOverlap = AttrMapValue(EitherOr((isNumberInRange(0.05,1),isBoolean)), desc="If true check and attempt to fix\n standard label overlaps(default off)",advancedUsage=1), 530 pointerLabelMode = AttrMapValue(OneOf(None,'LeftRight','LeftAndRight'), desc='',advancedUsage=1), 531 sameRadii = AttrMapValue(isBoolean, desc="If true make x/y radii the same(default off)",advancedUsage=1), 532 orderMode = AttrMapValue(OneOf('fixed','alternate'),advancedUsage=1), 533 xradius = AttrMapValue(isNumberOrNone, desc="X direction Radius"), 534 yradius = AttrMapValue(isNumberOrNone, desc="Y direction Radius"), 535 innerRadiusFraction = AttrMapValue(isNumberOrNone, desc="fraction of radii to start wedges at"), 536 wedgeRecord = AttrMapValue(None, desc="callable(wedge,*args,**kwds)",advancedUsage=1), 537 sideLabels = AttrMapValue(isBoolean, desc="If true attempt to make piechart with labels along side and pointers"), 538 sideLabelsOffset = AttrMapValue(isNumber, desc="The fraction of the pie width that the labels are situated at from the edges of the pie"), 539 ) 540 other_threshold=None 541 542 def __init__(self,**kwd): 543 PlotArea.__init__(self) 544 self.x = 0 545 self.y = 0 546 self.width = 100 547 self.height = 100 548 self.data = [1,2.3,1.7,4.2] 549 self.labels = None # or list of strings 550 self.startAngle = 90 551 self.direction = "clockwise" 552 self.simpleLabels = 1 553 self.checkLabelOverlap = 0 554 self.pointerLabelMode = None 555 self.sameRadii = False 556 self.orderMode = 'fixed' 557 self.xradius = self.yradius = self.innerRadiusFraction = None 558 self.sideLabels = 0 559 self.sideLabelsOffset = 0.1 560 561 self.slices = TypedPropertyCollection(WedgeProperties) 562 self.slices[0].fillColor = colors.darkcyan 563 self.slices[1].fillColor = colors.blueviolet 564 self.slices[2].fillColor = colors.blue 565 self.slices[3].fillColor = colors.cyan 566 self.slices[4].fillColor = colors.pink 567 self.slices[5].fillColor = colors.magenta 568 self.slices[6].fillColor = colors.yellow 569 570 def demo(self): 571 d = Drawing(200, 100) 572 573 pc = Pie() 574 pc.x = 50 575 pc.y = 10 576 pc.width = 100 577 pc.height = 80 578 pc.data = [10,20,30,40,50,60] 579 pc.labels = ['a','b','c','d','e','f'] 580 581 pc.slices.strokeWidth=0.5 582 pc.slices[3].popout = 10 583 pc.slices[3].strokeWidth = 2 584 pc.slices[3].strokeDashArray = [2,2] 585 pc.slices[3].labelRadius = 1.75 586 pc.slices[3].fontColor = colors.red 587 pc.slices[0].fillColor = colors.darkcyan 588 pc.slices[1].fillColor = colors.blueviolet 589 pc.slices[2].fillColor = colors.blue 590 pc.slices[3].fillColor = colors.cyan 591 pc.slices[4].fillColor = colors.aquamarine 592 pc.slices[5].fillColor = colors.cadetblue 593 pc.slices[6].fillColor = colors.lightcoral 594 595 d.add(pc) 596 return d 597 598 def makePointerLabels(self,angles,plMode): 599 class PL: 600 def __init__(self,centerx,centery,xradius,yradius,data,lu=0,ru=0): 601 self.centerx = centerx 602 self.centery = centery 603 self.xradius = xradius 604 self.yradius = yradius 605 self.data = data 606 self.lu = lu 607 self.ru = ru 608 609 labelX = self.width-2 610 labelY = self.height 611 n = nr = nl = maxW = sumH = 0 612 styleCount = len(self.slices) 613 L=[] 614 L_add = L.append 615 refArcs = _makeSideArcDefs(self.startAngle,self.direction) 616 for i, A in angles: 617 if A[1] is None: continue 618 sn = self.getSeriesName(i,'') 619 if not sn: continue 620 style = self.slices[i%styleCount] 621 if not style.label_visible or not style.visible: continue 622 n += 1 623 l=_addWedgeLabel(self,sn,180,labelX,labelY,style,labelClass=WedgeLabel) 624 L_add(l) 625 b = l.getBounds() 626 w = b[2]-b[0] 627 h = b[3]-b[1] 628 ri = [(a[0],intervalIntersection(A,(a[1],a[2]))) for a in refArcs] 629 li = _findLargestArc(ri,0) 630 ri = _findLargestArc(ri,1) 631 if li and ri: 632 if plMode=='LeftAndRight': 633 if li[1]-li[0]<ri[1]-ri[0]: 634 li = None 635 else: 636 ri = None 637 else: 638 if li[1]-li[0]<0.02*(ri[1]-ri[0]): 639 li = None 640 elif (li[1]-li[0])*0.02>ri[1]-ri[0]: 641 ri = None 642 if ri: nr += 1 643 if li: nl += 1 644 l._origdata = dict(bounds=b,width=w,height=h,li=li,ri=ri,index=i,edgePad=style.label_pointer_edgePad,piePad=style.label_pointer_piePad,elbowLength=style.label_pointer_elbowLength) 645 maxW = max(w,maxW) 646 sumH += h+2 647 648 if not n: #we have no labels 649 xradius = self.width*0.5 650 yradius = self.height*0.5 651 centerx = self.x+xradius 652 centery = self.y+yradius 653 if self.xradius: xradius = self.xradius 654 if self.yradius: yradius = self.yradius 655 if self.sameRadii: xradius=yradius=min(xradius,yradius) 656 return PL(centerx,centery,xradius,yradius,[]) 657 658 aonR = nr==n 659 if sumH<self.height and (aonR or nl==n): 660 side=int(aonR) 661 else: 662 side=None 663 G,lu,ru,mel = _fixPointerLabels(len(angles),L,self.x,self.y,self.width,self.height,side=side) 664 if plMode=='LeftAndRight': 665 lu = ru = max(lu,ru) 666 x0 = self.x+lu 667 x1 = self.x+self.width-ru 668 xradius = (x1-x0)*0.5 669 yradius = self.height*0.5-mel 670 centerx = x0+xradius 671 centery = self.y+yradius+mel 672 if self.xradius: xradius = self.xradius 673 if self.yradius: yradius = self.yradius 674 if self.sameRadii: xradius=yradius=min(xradius,yradius) 675 return PL(centerx,centery,xradius,yradius,G,lu,ru) 676 677 def normalizeData(self,keepData=False): 678 data = list(map(abs,self.data)) 679 s = self._sum = float(sum(data)) 680 f = 360./s if s!=0 else 1 681 if keepData: 682 return [AngleData(f*x,x) for x in data] 683 else: 684 return [f*x for x in data] 685 686 def makeAngles(self): 687 wr = getattr(self,'wedgeRecord',None) 688 if self.sideLabels: 689 startAngle = theta0(self.data, self.direction) 690 self.slices.label_visible = 1 691 else: 692 startAngle = self.startAngle % 360 693 whichWay = self.direction == "clockwise" and -1 or 1 694 D = [a for a in enumerate(self.normalizeData(keepData=wr))] 695 if self.orderMode=='alternate' and not self.sideLabels: 696 W = [a for a in D if abs(a[1])>=1e-5] 697 W.sort(key=_arcCF) 698 T = [[],[]] 699 i = 0 700 while W: 701 if i<2: 702 a = W.pop(0) 703 else: 704 a = W.pop(-1) 705 T[i%2].append(a) 706 i += 1 707 i %= 4 708 T[1].reverse() 709 D = T[0]+T[1] + [a for a in D if abs(a[1])<1e-5] 710 A = [] 711 a = A.append 712 for i, angle in D: 713 endAngle = (startAngle + (angle * whichWay)) 714 if abs(angle)>=_ANGLELO: 715 if startAngle >= endAngle: 716 aa = endAngle,startAngle 717 else: 718 aa = startAngle,endAngle 719 else: 720 aa = startAngle, None 721 if wr: 722 aa = (AngleData(aa[0],angle._data),aa[1]) 723 startAngle = endAngle 724 a((i,aa)) 725 return A 726 727 def makeWedges(self): 728 angles = self.makeAngles() 729 #Checking to see whether there are too many wedges packed in too small a space 730 halfAngles = [] 731 for i,(a1,a2) in angles: 732 if a2 is None: 733 halfAngle = a1 734 else: 735 halfAngle = 0.5*(a2+a1) 736 halfAngles.append(halfAngle) 737 sideLabels = self.sideLabels 738 n = len(angles) 739 labels = _fixLabels(self.labels,n) 740 wr = getattr(self,'wedgeRecord',None) 741 742 self._seriesCount = n 743 styleCount = len(self.slices) 744 745 plMode = self.pointerLabelMode 746 if sideLabels: 747 plMode = None 748 if plMode: 749 checkLabelOverlap = False 750 PL=self.makePointerLabels(angles,plMode) 751 xradius = PL.xradius 752 yradius = PL.yradius 753 centerx = PL.centerx 754 centery = PL.centery 755 PL_data = PL.data 756 gSN = lambda i: '' 757 else: 758 xradius = self.width*0.5 759 yradius = self.height*0.5 760 centerx = self.x + xradius 761 centery = self.y + yradius 762 if self.xradius: xradius = self.xradius 763 if self.yradius: yradius = self.yradius 764 if self.sameRadii: xradius=yradius=min(xradius,yradius) 765 checkLabelOverlap = self.checkLabelOverlap 766 gSN = lambda i: self.getSeriesName(i,'') 767 768 g = Group() 769 g_add = g.add 770 L = [] 771 L_add = L.append 772 773 innerRadiusFraction = self.innerRadiusFraction 774 775 776 for i,(a1,a2) in angles: 777 if a2 is None: continue 778 #if we didn't use %stylecount here we'd end up with the later wedges 779 #all having the default style 780 wedgeStyle = self.slices[i%styleCount] 781 if not wedgeStyle.visible: continue 782 aa = abs(a2-a1) 783 784 # is it a popout? 785 cx, cy = centerx, centery 786 text = gSN(i) 787 popout = wedgeStyle.popout 788 if text or popout: 789 averageAngle = (a1+a2)/2.0 790 aveAngleRadians = averageAngle/_180_pi 791 cosAA = cos(aveAngleRadians) 792 sinAA = sin(aveAngleRadians) 793 if popout and aa<_ANGLEHI: 794 # pop out the wedge 795 cx = centerx + popout*cosAA 796 cy = centery + popout*sinAA 797 798 if innerRadiusFraction: 799 theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, 800 radius1=xradius*innerRadiusFraction,yradius1=yradius*innerRadiusFraction) 801 else: 802 if aa>=_ANGLEHI: 803 theWedge = Ellipse(cx, cy, xradius, yradius) 804 else: 805 theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius) 806 807 808 theWedge.fillColor = wedgeStyle.fillColor 809 theWedge.strokeColor = wedgeStyle.strokeColor 810 theWedge.strokeWidth = wedgeStyle.strokeWidth 811 theWedge.strokeLineJoin = wedgeStyle.strokeLineJoin 812 theWedge.strokeLineCap = wedgeStyle.strokeLineCap 813 theWedge.strokeMiterLimit = wedgeStyle.strokeMiterLimit 814 theWedge.strokeDashArray = wedgeStyle.strokeDashArray 815 816 shader = wedgeStyle.shadingKind 817 if shader: 818 nshades = aa / float(wedgeStyle.shadingAngle) 819 if nshades > 1: 820 shader = colors.Whiter if shader=='lighten' else colors.Blacker 821 nshades = 1+int(nshades) 822 shadingAmount = 1-wedgeStyle.shadingAmount 823 if wedgeStyle.shadingDirection=='normal': 824 dsh = (1-shadingAmount)/float(nshades-1) 825 shf1 = shadingAmount 826 else: 827 dsh = (shadingAmount-1)/float(nshades-1) 828 shf1 = 1 829 shda = (a2-a1)/float(nshades) 830 shsc = wedgeStyle.fillColor 831 theWedge.fillColor = None 832 for ish in xrange(nshades): 833 sha1 = a1 + ish*shda 834 sha2 = a1 + (ish+1)*shda 835 shc = shader(shsc,shf1 + dsh*ish) 836 if innerRadiusFraction: 837 shWedge = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, 838 radius1=xradius*innerRadiusFraction,yradius1=yradius*innerRadiusFraction) 839 else: 840 shWedge = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius) 841 shWedge.fillColor = shc 842 shWedge.strokeColor = None 843 shWedge.strokeWidth = 0 844 g_add(shWedge) 845 846 g_add(theWedge) 847 if wr: 848 wr(theWedge,value=a1._data,label=text) 849 if wedgeStyle.label_visible: 850 if not sideLabels: 851 if text: 852 labelRadius = wedgeStyle.labelRadius 853 rx = xradius*labelRadius 854 ry = yradius*labelRadius 855 labelX = cx + rx*cosAA 856 labelY = cy + ry*sinAA 857 l = _addWedgeLabel(self,text,averageAngle,labelX,labelY,wedgeStyle) 858 L_add(l) 859 if not plMode and l._simple_pointer: 860 l._aax = cx+xradius*cosAA 861 l._aay = cy+yradius*sinAA 862 if checkLabelOverlap: 863 l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle, 864 'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy, 865 'bounds': l.getBounds(), 'angles':(a1,a2), 866 } 867 elif plMode and PL_data: 868 l = PL_data[i] 869 if l: 870 data = l._origdata 871 sinM = data['smid'] 872 cosM = data['cmid'] 873 lX = cx + xradius*cosM 874 lY = cy + yradius*sinM 875 lpel = wedgeStyle.label_pointer_elbowLength 876 lXi = lX + lpel*cosM 877 lYi = lY + lpel*sinM 878 L_add(PolyLine((lX,lY,lXi,lYi,l.x,l.y), 879 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 880 strokeColor=wedgeStyle.label_pointer_strokeColor)) 881 L_add(l) 882 else: 883 if text: 884 slices_popout = self.slices.popout 885 m=0 886 for n, angle in angles: 887 if self.slices[n].fillColor: 888 m += 1 889 else: 890 r = n%m 891 self.slices[n].fillColor = self.slices[r].fillColor 892 self.slices[n].popout = self.slices[r].popout 893 for j in range(0,m-1): 894 if self.slices[j].popout > slices_popout: 895 slices_popout = self.slices[j].popout 896 labelRadius = wedgeStyle.labelRadius 897 ry = yradius*labelRadius 898 if (abs(averageAngle) < 90 ) or (averageAngle >270 and averageAngle <450) or (-450< 899 averageAngle <-270): 900 labelX = (1+self.sideLabelsOffset)*self.width + self.x + slices_popout 901 rx = 0 902 else: 903 labelX = self.x - (self.sideLabelsOffset)*self.width - slices_popout 904 rx = 0 905 labelY = cy + ry*sinAA 906 l = _addWedgeLabel(self,text,averageAngle,labelX,labelY,wedgeStyle) 907 L_add(l) 908 if not plMode: 909 l._aax = cx+xradius*cosAA 910 l._aay = cy+yradius*sinAA 911 if checkLabelOverlap: 912 l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle, 913 'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy, 914 'bounds': l.getBounds(), 915 } 916 x1,y1,x2,y2 = l.getBounds() 917 918 if checkLabelOverlap and L: 919 fixLabelOverlaps(L, sideLabels, mult0=checkLabelOverlap) 920 for l in L: g_add(l) 921 922 if not plMode: 923 for l in L: 924 if l._simple_pointer and not sideLabels: 925 g_add(Line(l.x,l.y,l._aax,l._aay, 926 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 927 strokeColor=wedgeStyle.label_pointer_strokeColor)) 928 elif sideLabels: 929 x1,y1,x2,y2 = l.getBounds() 930 #add pointers 931 if l.x == (1+self.sideLabelsOffset)*self.width + self.x: 932 g_add(Line(l._aax,l._aay,0.5*(l._aax+l.x),l.y+(0.25*(y2-y1)), 933 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 934 strokeColor=wedgeStyle.label_pointer_strokeColor)) 935 g_add(Line(0.5*(l._aax+l.x),l.y+(0.25*(y2-y1)),l.x,l.y+(0.25*(y2-y1)), 936 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 937 strokeColor=wedgeStyle.label_pointer_strokeColor)) 938 else: 939 g_add(Line(l._aax,l._aay,0.5*(l._aax+l.x),l.y+(0.25*(y2-y1)), 940 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 941 strokeColor=wedgeStyle.label_pointer_strokeColor)) 942 g_add(Line(0.5*(l._aax+l.x),l.y+(0.25*(y2-y1)),l.x,l.y+(0.25*(y2-y1)), 943 strokeWidth=wedgeStyle.label_pointer_strokeWidth, 944 strokeColor=wedgeStyle.label_pointer_strokeColor)) 945 946 return g 947 948 def draw(self): 949 G = self.makeBackground() 950 w = self.makeWedges() 951 if G: return Group(G,w) 952 return w 953 954class LegendedPie(Pie): 955 """Pie with a two part legend (one editable with swatches, one hidden without swatches).""" 956 957 _attrMap = AttrMap(BASE=Pie, 958 drawLegend = AttrMapValue(isBoolean, desc="If true then create and draw legend"), 959 legend1 = AttrMapValue(None, desc="Handle to legend for pie"), 960 legendNumberFormat = AttrMapValue(None, desc="Formatting routine for number on right hand side of legend."), 961 legendNumberOffset = AttrMapValue(isNumber, desc="Horizontal space between legend and numbers on r/hand side"), 962 pieAndLegend_colors = AttrMapValue(isListOfColors, desc="Colours used for both swatches and pie"), 963 legend_names = AttrMapValue(isNoneOrListOfNoneOrStrings, desc="Names used in legend (or None)"), 964 legend_data = AttrMapValue(isNoneOrListOfNoneOrNumbers, desc="Numbers used on r/hand side of legend (or None)"), 965 leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), 966 rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), 967 topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), 968 bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), 969 ) 970 971 def __init__(self): 972 Pie.__init__(self) 973 self.x = 0 974 self.y = 0 975 self.height = 100 976 self.width = 100 977 self.data = [38.4, 20.7, 18.9, 15.4, 6.6] 978 self.labels = None 979 self.direction = 'clockwise' 980 PCMYKColor, black = colors.PCMYKColor, colors.black 981 self.pieAndLegend_colors = [PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV'), 982 PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV'), 983 PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=75), 984 PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=75), 985 PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=50), 986 PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=50)] 987 988 #Allows us up to six 'wedges' to be coloured 989 self.slices[0].fillColor=self.pieAndLegend_colors[0] 990 self.slices[1].fillColor=self.pieAndLegend_colors[1] 991 self.slices[2].fillColor=self.pieAndLegend_colors[2] 992 self.slices[3].fillColor=self.pieAndLegend_colors[3] 993 self.slices[4].fillColor=self.pieAndLegend_colors[4] 994 self.slices[5].fillColor=self.pieAndLegend_colors[5] 995 996 self.slices.strokeWidth = 0.75 997 self.slices.strokeColor = black 998 999 legendOffset = 17 1000 self.legendNumberOffset = 51 1001 self.legendNumberFormat = '%.1f%%' 1002 self.legend_data = self.data 1003 1004 #set up the legends 1005 from reportlab.graphics.charts.legends import Legend 1006 self.legend1 = Legend() 1007 self.legend1.x = self.width+legendOffset 1008 self.legend1.y = self.height 1009 self.legend1.deltax = 5.67 1010 self.legend1.deltay = 14.17 1011 self.legend1.dxTextSpace = 11.39 1012 self.legend1.dx = 5.67 1013 self.legend1.dy = 5.67 1014 self.legend1.columnMaximum = 7 1015 self.legend1.alignment = 'right' 1016 self.legend_names = ['AAA:','AA:','A:','BBB:','NR:'] 1017 for f in range(len(self.data)): 1018 self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) 1019 self.legend1.fontName = "Helvetica-Bold" 1020 self.legend1.fontSize = 6 1021 self.legend1.strokeColor = black 1022 self.legend1.strokeWidth = 0.5 1023 1024 self._legend2 = Legend() 1025 self._legend2.dxTextSpace = 0 1026 self._legend2.dx = 0 1027 self._legend2.alignment = 'right' 1028 self._legend2.fontName = "Helvetica-Oblique" 1029 self._legend2.fontSize = 6 1030 self._legend2.strokeColor = self.legend1.strokeColor 1031 1032 self.leftPadding = 5 1033 self.rightPadding = 5 1034 self.topPadding = 5 1035 self.bottomPadding = 5 1036 self.drawLegend = 1 1037 1038 def draw(self): 1039 if self.drawLegend: 1040 self.legend1.colorNamePairs = [] 1041 self._legend2.colorNamePairs = [] 1042 for f in range(len(self.data)): 1043 if self.legend_names == None: 1044 self.slices[f].fillColor = self.pieAndLegend_colors[f] 1045 self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], None)) 1046 else: 1047 try: 1048 self.slices[f].fillColor = self.pieAndLegend_colors[f] 1049 self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) 1050 except IndexError: 1051 self.slices[f].fillColor = self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)] 1052 self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)], self.legend_names[f])) 1053 if self.legend_data != None: 1054 ldf = self.legend_data[f] 1055 lNF = self.legendNumberFormat 1056 if ldf is None or lNF is None: 1057 pass 1058 elif isinstance(lNF,str): 1059 ldf = lNF % ldf 1060 elif hasattr(lNF,'__call__'): 1061 ldf = lNF(ldf) 1062 else: 1063 raise ValueError("Unknown formatter type %s, expected string or function" % ascii(self.legendNumberFormat)) 1064 self._legend2.colorNamePairs.append((None,ldf)) 1065 p = Pie.draw(self) 1066 if self.drawLegend: 1067 p.add(self.legend1) 1068 #hide from user - keeps both sides lined up! 1069 self._legend2.x = self.legend1.x+self.legendNumberOffset 1070 self._legend2.y = self.legend1.y 1071 self._legend2.deltax = self.legend1.deltax 1072 self._legend2.deltay = self.legend1.deltay 1073 self._legend2.dy = self.legend1.dy 1074 self._legend2.columnMaximum = self.legend1.columnMaximum 1075 p.add(self._legend2) 1076 p.shift(self.leftPadding, self.bottomPadding) 1077 return p 1078 1079 def _getDrawingDimensions(self): 1080 tx = self.rightPadding 1081 if self.drawLegend: 1082 tx += self.legend1.x+self.legendNumberOffset #self._legend2.x 1083 tx += self._legend2._calculateMaxWidth(self._legend2.colorNamePairs) 1084 ty = self.bottomPadding+self.height+self.topPadding 1085 return (tx,ty) 1086 1087 def demo(self, drawing=None): 1088 if not drawing: 1089 tx,ty = self._getDrawingDimensions() 1090 drawing = Drawing(tx, ty) 1091 drawing.add(self.draw()) 1092 return drawing 1093 1094from reportlab.graphics.charts.utils3d import _getShaded, _2rad, _360, _pi_2, _2pi, _180_pi 1095class Wedge3dProperties(PropHolder): 1096 """This holds descriptive information about the wedges in a pie chart. 1097 1098 It is not to be confused with the 'wedge itself'; this just holds 1099 a recipe for how to format one, and does not allow you to hack the 1100 angles. It can format a genuine Wedge object for you with its 1101 format method. 1102 """ 1103 _attrMap = AttrMap( 1104 fillColor = AttrMapValue(isColorOrNone,desc=''), 1105 fillColorShaded = AttrMapValue(isColorOrNone,desc=''), 1106 fontColor = AttrMapValue(isColorOrNone,desc=''), 1107 fontName = AttrMapValue(isString,desc=''), 1108 fontSize = AttrMapValue(isNumber,desc=''), 1109 label_angle = AttrMapValue(isNumber,desc=''), 1110 label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), 1111 label_boxAnchor = AttrMapValue(isBoxAnchor,desc=''), 1112 label_boxFillColor = AttrMapValue(isColorOrNone,desc=''), 1113 label_boxStrokeColor = AttrMapValue(isColorOrNone,desc=''), 1114 label_boxStrokeWidth = AttrMapValue(isNumber,desc=''), 1115 label_dx = AttrMapValue(isNumber,desc=''), 1116 label_dy = AttrMapValue(isNumber,desc=''), 1117 label_height = AttrMapValue(isNumberOrNone,desc=''), 1118 label_leading = AttrMapValue(isNumberOrNone,desc=''), 1119 label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), 1120 label_maxWidth = AttrMapValue(isNumberOrNone,desc=''), 1121 label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), 1122 label_simple_pointer = AttrMapValue(isBoolean,'set to True for simple pointers'), 1123 label_strokeColor = AttrMapValue(isColorOrNone,desc=''), 1124 label_strokeWidth = AttrMapValue(isNumber,desc=''), 1125 label_text = AttrMapValue(isStringOrNone,desc=''), 1126 label_textAnchor = AttrMapValue(isTextAnchor,desc=''), 1127 label_topPadding = AttrMapValue(isNumber,'padding at top of box'), 1128 label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), 1129 label_width = AttrMapValue(isNumberOrNone,desc=''), 1130 labelRadius = AttrMapValue(isNumber,desc=''), 1131 popout = AttrMapValue(isNumber,desc=''), 1132 shading = AttrMapValue(isNumber,desc=''), 1133 strokeColor = AttrMapValue(isColorOrNone,desc=''), 1134 strokeColorShaded = AttrMapValue(isColorOrNone,desc=''), 1135 strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc=''), 1136 strokeWidth = AttrMapValue(isNumber,desc=''), 1137 visible = AttrMapValue(isBoolean,'set to false to skip displaying'), 1138 ) 1139 1140 def __init__(self): 1141 self.strokeWidth = 0 1142 self.shading = 0.3 1143 self.visible = 1 1144 self.strokeColorShaded = self.fillColorShaded = self.fillColor = None 1145 self.strokeColor = STATE_DEFAULTS["strokeColor"] 1146 self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] 1147 self.popout = 0 1148 self.fontName = STATE_DEFAULTS["fontName"] 1149 self.fontSize = STATE_DEFAULTS["fontSize"] 1150 self.fontColor = STATE_DEFAULTS["fillColor"] 1151 self.labelRadius = 1.2 1152 self.label_dx = self.label_dy = self.label_angle = 0 1153 self.label_text = None 1154 self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 1155 self.label_boxAnchor = 'autox' 1156 self.label_boxStrokeColor = None #boxStroke 1157 self.label_boxStrokeWidth = 0.5 #boxStrokeWidth 1158 self.label_boxFillColor = None 1159 self.label_strokeColor = None 1160 self.label_strokeWidth = 0.1 1161 self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None 1162 self.label_textAnchor = 'start' 1163 self.label_visible = 1 1164 self.label_simple_pointer = 0 1165 1166class _SL3D: 1167 def __init__(self,lo,hi): 1168 if lo<0: 1169 lo += 360 1170 hi += 360 1171 self.lo = lo 1172 self.hi = hi 1173 self.mid = (lo+hi)*0.5 1174 self.not360 = abs(hi-lo) < _ANGLEHI 1175 1176 def __str__(self): 1177 return '_SL3D(%.2f,%.2f)' % (self.lo,self.hi) 1178 1179def _keyS3D(a,b): 1180 return -cmp(a[0],b[0]) 1181_keyS3D = functools.cmp_to_key(_keyS3D) 1182 1183_270r = _2rad(270) 1184class Pie3d(Pie): 1185 _attrMap = AttrMap(BASE=Pie, 1186 perspective = AttrMapValue(isNumber, desc='A flattening parameter.'), 1187 depth_3d = AttrMapValue(isNumber, desc='depth of the pie.'), 1188 angle_3d = AttrMapValue(isNumber, desc='The view angle.'), 1189 ) 1190 perspective = 70 1191 depth_3d = 25 1192 angle_3d = 180 1193 1194 def _popout(self,i): 1195 return self._sl3d[i].not360 and self.slices[i].popout or 0 1196 1197 def CX(self, i,d ): 1198 return self._cx+(d and self._xdepth_3d or 0)+self._popout(i)*cos(_2rad(self._sl3d[i].mid)) 1199 def CY(self,i,d): 1200 return self._cy+(d and self._ydepth_3d or 0)+self._popout(i)*sin(_2rad(self._sl3d[i].mid)) 1201 def OX(self,i,o,d): 1202 return self.CX(i,d)+self._radiusx*cos(_2rad(o)) 1203 def OY(self,i,o,d): 1204 return self.CY(i,d)+self._radiusy*sin(_2rad(o)) 1205 1206 def rad_dist(self,a): 1207 _3dva = self._3dva 1208 return min(abs(a-_3dva),abs(a-_3dva+360)) 1209 1210 def __init__(self): 1211 Pie.__init__(self) 1212 self.slices = TypedPropertyCollection(Wedge3dProperties) 1213 self.slices[0].fillColor = colors.darkcyan 1214 self.slices[1].fillColor = colors.blueviolet 1215 self.slices[2].fillColor = colors.blue 1216 self.slices[3].fillColor = colors.cyan 1217 self.slices[4].fillColor = colors.azure 1218 self.slices[5].fillColor = colors.crimson 1219 self.slices[6].fillColor = colors.darkviolet 1220 self.xradius = self.yradius = None 1221 self.width = 300 1222 self.height = 200 1223 self.data = [12.50,20.10,2.00,22.00,5.00,18.00,13.00] 1224 1225 def _fillSide(self,L,i,angle,strokeColor,strokeWidth,fillColor): 1226 rd = self.rad_dist(angle) 1227 if rd<self.rad_dist(self._sl3d[i].mid): 1228 p = [self.CX(i,0),self.CY(i,0), 1229 self.CX(i,1),self.CY(i,1), 1230 self.OX(i,angle,1),self.OY(i,angle,1), 1231 self.OX(i,angle,0),self.OY(i,angle,0)] 1232 L.append((rd,Polygon(p, strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1))) 1233 1234 def draw(self): 1235 slices = self.slices 1236 _3d_angle = self.angle_3d 1237 _3dva = self._3dva = _360(_3d_angle+90) 1238 a0 = _2rad(_3dva) 1239 depth_3d = self.depth_3d 1240 self._xdepth_3d = cos(a0)*depth_3d 1241 self._ydepth_3d = sin(a0)*depth_3d 1242 self._cx = self.x+self.width/2.0 1243 self._cy = self.y+(self.height - self._ydepth_3d)/2.0 1244 radiusx = radiusy = self._cx-self.x 1245 if self.xradius: radiusx = self.xradius 1246 if self.yradius: radiusy = self.yradius 1247 self._radiusx = radiusx 1248 self._radiusy = radiusy = (1.0 - self.perspective/100.0)*radiusy 1249 data = self.normalizeData() 1250 sum = self._sum 1251 1252 CX = self.CX 1253 CY = self.CY 1254 OX = self.OX 1255 OY = self.OY 1256 rad_dist = self.rad_dist 1257 _fillSide = self._fillSide 1258 self._seriesCount = n = len(data) 1259 _sl3d = self._sl3d = [] 1260 g = Group() 1261 last = _360(self.startAngle) 1262 a0 = self.direction=='clockwise' and -1 or 1 1263 for v in data: 1264 v *= a0 1265 angle1, angle0 = last, v+last 1266 last = angle0 1267 if a0>0: angle0, angle1 = angle1, angle0 1268 _sl3d.append(_SL3D(angle0,angle1)) 1269 1270 labels = _fixLabels(self.labels,n) 1271 a0 = _3d_angle 1272 a1 = _3d_angle+180 1273 T = [] 1274 S = [] 1275 L = [] 1276 1277 class WedgeLabel3d(WedgeLabel): 1278 _ydepth_3d = self._ydepth_3d 1279 def _checkDXY(self,ba): 1280 if ba[0]=='n': 1281 if not hasattr(self,'_ody'): 1282 self._ody = self.dy 1283 self.dy = -self._ody + self._ydepth_3d 1284 1285 checkLabelOverlap = self.checkLabelOverlap 1286 1287 for i in range(n): 1288 style = slices[i] 1289 if not style.visible: continue 1290 sl = _sl3d[i] 1291 lo = angle0 = sl.lo 1292 hi = angle1 = sl.hi 1293 aa = abs(hi-lo) 1294 if aa<_ANGLELO: continue 1295 fillColor = _getShaded(style.fillColor,style.fillColorShaded,style.shading) 1296 strokeColor = _getShaded(style.strokeColor,style.strokeColorShaded,style.shading) or fillColor 1297 strokeWidth = style.strokeWidth 1298 cx0 = CX(i,0) 1299 cy0 = CY(i,0) 1300 cx1 = CX(i,1) 1301 cy1 = CY(i,1) 1302 if depth_3d: 1303 #background shaded pie bottom 1304 g.add(Wedge(cx1,cy1,radiusx, lo, hi,yradius=radiusy, 1305 strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor, 1306 strokeLineJoin=1)) 1307 #connect to top 1308 if lo < a0 < hi: angle0 = a0 1309 if lo < a1 < hi: angle1 = a1 1310 p = ArcPath(strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1) 1311 p.addArc(cx1,cy1,radiusx,angle0,angle1,yradius=radiusy,moveTo=1) 1312 p.lineTo(OX(i,angle1,0),OY(i,angle1,0)) 1313 p.addArc(cx0,cy0,radiusx,angle0,angle1,yradius=radiusy,reverse=1) 1314 p.closePath() 1315 if angle0<=_3dva and angle1>=_3dva: 1316 rd = 0 1317 else: 1318 rd = min(rad_dist(angle0),rad_dist(angle1)) 1319 S.append((rd,p)) 1320 _fillSide(S,i,lo,strokeColor,strokeWidth,fillColor) 1321 _fillSide(S,i,hi,strokeColor,strokeWidth,fillColor) 1322 1323 #bright shaded top 1324 fillColor = style.fillColor 1325 strokeColor = style.strokeColor or fillColor 1326 T.append(Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy, 1327 strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1)) 1328 if aa>=_ANGLEHI: 1329 theWedge = Ellipse(cx0, cy0, radiusx, radiusy, 1330 strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1) 1331 else: 1332 theWedge = Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy, 1333 strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1) 1334 T.append(theWedge) 1335 1336 text = labels[i] 1337 if style.label_visible and text: 1338 rat = style.labelRadius 1339 self._radiusx *= rat 1340 self._radiusy *= rat 1341 mid = sl.mid 1342 labelX = OX(i,mid,0) 1343 labelY = OY(i,mid,0) 1344 l=_addWedgeLabel(self,text,mid,labelX,labelY,style,labelClass=WedgeLabel3d) 1345 L.append(l) 1346 if checkLabelOverlap: 1347 l._origdata = { 'x': labelX, 'y':labelY, 'angle': mid, 1348 'rx': self._radiusx, 'ry':self._radiusy, 'cx':CX(i,0), 'cy':CY(i,0), 1349 'bounds': l.getBounds(), 1350 } 1351 self._radiusx = radiusx 1352 self._radiusy = radiusy 1353 1354 S.sort(key=_keyS3D) 1355 if checkLabelOverlap and L: 1356 fixLabelOverlaps(L,self.sideLabels) 1357 for x in ([s[1] for s in S]+T+L): 1358 g.add(x) 1359 return g 1360 1361 def demo(self): 1362 d = Drawing(200, 100) 1363 1364 pc = Pie() 1365 pc.x = 50 1366 pc.y = 10 1367 pc.width = 100 1368 pc.height = 80 1369 pc.data = [10,20,30,40,50,60] 1370 pc.labels = ['a','b','c','d','e','f'] 1371 1372 pc.slices.strokeWidth=0.5 1373 pc.slices[3].popout = 10 1374 pc.slices[3].strokeWidth = 2 1375 pc.slices[3].strokeDashArray = [2,2] 1376 pc.slices[3].labelRadius = 1.75 1377 pc.slices[3].fontColor = colors.red 1378 pc.slices[0].fillColor = colors.darkcyan 1379 pc.slices[1].fillColor = colors.blueviolet 1380 pc.slices[2].fillColor = colors.blue 1381 pc.slices[3].fillColor = colors.cyan 1382 pc.slices[4].fillColor = colors.aquamarine 1383 pc.slices[5].fillColor = colors.cadetblue 1384 pc.slices[6].fillColor = colors.lightcoral 1385 self.slices[1].visible = 0 1386 self.slices[3].visible = 1 1387 self.slices[4].visible = 1 1388 self.slices[5].visible = 1 1389 self.slices[6].visible = 0 1390 1391 d.add(pc) 1392 return d 1393 1394 1395def sample0a(): 1396 "Make a degenerated pie chart with only one slice." 1397 1398 d = Drawing(400, 200) 1399 1400 pc = Pie() 1401 pc.x = 150 1402 pc.y = 50 1403 pc.data = [10] 1404 pc.labels = ['a'] 1405 pc.slices.strokeWidth=1#0.5 1406 1407 d.add(pc) 1408 1409 return d 1410 1411 1412def sample0b(): 1413 "Make a degenerated pie chart with only one slice." 1414 1415 d = Drawing(400, 200) 1416 1417 pc = Pie() 1418 pc.x = 150 1419 pc.y = 50 1420 pc.width = 120 1421 pc.height = 100 1422 pc.data = [10] 1423 pc.labels = ['a'] 1424 pc.slices.strokeWidth=1#0.5 1425 1426 d.add(pc) 1427 1428 return d 1429 1430 1431def sample1(): 1432 "Make a typical pie chart with with one slice treated in a special way." 1433 1434 d = Drawing(400, 200) 1435 1436 pc = Pie() 1437 pc.x = 150 1438 pc.y = 50 1439 pc.data = [10, 20, 30, 40, 50, 60] 1440 pc.labels = ['a', 'b', 'c', 'd', 'e', 'f'] 1441 1442 pc.slices.strokeWidth=1#0.5 1443 pc.slices[3].popout = 20 1444 pc.slices[3].strokeWidth = 2 1445 pc.slices[3].strokeDashArray = [2,2] 1446 pc.slices[3].labelRadius = 1.75 1447 pc.slices[3].fontColor = colors.red 1448 1449 d.add(pc) 1450 1451 return d 1452 1453 1454def sample2(): 1455 "Make a pie chart with nine slices." 1456 1457 d = Drawing(400, 200) 1458 1459 pc = Pie() 1460 pc.x = 125 1461 pc.y = 25 1462 pc.data = [0.31, 0.148, 0.108, 1463 0.076, 0.033, 0.03, 1464 0.019, 0.126, 0.15] 1465 pc.labels = ['1', '2', '3', '4', '5', '6', '7', '8', 'X'] 1466 1467 pc.width = 150 1468 pc.height = 150 1469 pc.slices.strokeWidth=1#0.5 1470 1471 pc.slices[0].fillColor = colors.steelblue 1472 pc.slices[1].fillColor = colors.thistle 1473 pc.slices[2].fillColor = colors.cornflower 1474 pc.slices[3].fillColor = colors.lightsteelblue 1475 pc.slices[4].fillColor = colors.aquamarine 1476 pc.slices[5].fillColor = colors.cadetblue 1477 pc.slices[6].fillColor = colors.lightcoral 1478 pc.slices[7].fillColor = colors.tan 1479 pc.slices[8].fillColor = colors.darkseagreen 1480 1481 d.add(pc) 1482 1483 return d 1484 1485 1486def sample3(): 1487 "Make a pie chart with a very slim slice." 1488 1489 d = Drawing(400, 200) 1490 1491 pc = Pie() 1492 pc.x = 125 1493 pc.y = 25 1494 1495 pc.data = [74, 1, 25] 1496 1497 pc.width = 150 1498 pc.height = 150 1499 pc.slices.strokeWidth=1#0.5 1500 pc.slices[0].fillColor = colors.steelblue 1501 pc.slices[1].fillColor = colors.thistle 1502 pc.slices[2].fillColor = colors.cornflower 1503 1504 d.add(pc) 1505 1506 return d 1507 1508 1509def sample4(): 1510 "Make a pie chart with several very slim slices." 1511 1512 d = Drawing(400, 200) 1513 1514 pc = Pie() 1515 pc.x = 125 1516 pc.y = 25 1517 1518 pc.data = [74, 1, 1, 1, 1, 22] 1519 1520 pc.width = 150 1521 pc.height = 150 1522 pc.slices.strokeWidth=1#0.5 1523 pc.slices[0].fillColor = colors.steelblue 1524 pc.slices[1].fillColor = colors.thistle 1525 pc.slices[2].fillColor = colors.cornflower 1526 pc.slices[3].fillColor = colors.lightsteelblue 1527 pc.slices[4].fillColor = colors.aquamarine 1528 pc.slices[5].fillColor = colors.cadetblue 1529 1530 d.add(pc) 1531 1532 return d 1533 1534def sample5(): 1535 "Make a pie with side labels." 1536 1537 d = Drawing(400, 200) 1538 1539 pc = Pie() 1540 pc.x = 125 1541 pc.y = 25 1542 1543 pc.data = [7, 1, 1, 1, 1, 2] 1544 pc.labels = ['example1', 'example2', 'example3', 'example4', 'example5', 'example6'] 1545 pc.sideLabels = 1 1546 1547 pc.width = 150 1548 pc.height = 150 1549 pc.slices.strokeWidth=1#0.5 1550 pc.slices[0].fillColor = colors.steelblue 1551 pc.slices[1].fillColor = colors.thistle 1552 pc.slices[2].fillColor = colors.cornflower 1553 pc.slices[3].fillColor = colors.lightsteelblue 1554 pc.slices[4].fillColor = colors.aquamarine 1555 pc.slices[5].fillColor = colors.cadetblue 1556 1557 d.add(pc) 1558 1559 return d 1560 1561def sample6(): 1562 1563 "Illustrates the pie moving to leave space for the left labels" 1564 1565 d = Drawing(400, 200) 1566 1567 pc = Pie() 1568 "The x value of the pie chart is 0" 1569 pc.x = 0 1570 pc.y = 25 1571 1572 pc.data = [74, 1, 1, 1, 1, 22] 1573 pc.labels = ['example1', 'example2', 'example3', 'example4', 'example5', 'example6'] 1574 pc.sideLabels = 1 1575 1576 pc.width = 150 1577 pc.height = 150 1578 pc.slices.strokeWidth=1#0.5 1579 pc.slices[0].fillColor = colors.steelblue 1580 pc.slices[1].fillColor = colors.thistle 1581 pc.slices[2].fillColor = colors.cornflower 1582 pc.slices[3].fillColor = colors.lightsteelblue 1583 pc.slices[4].fillColor = colors.aquamarine 1584 pc.slices[5].fillColor = colors.cadetblue 1585 1586 l = Line(0,0,0,200) 1587 1588 d.add(pc) 1589 d.add(l) 1590 1591 return d 1592 1593def sample7(): 1594 1595 "Case with overlapping pointers" 1596 1597 d = Drawing(400, 200) 1598 1599 pc = Pie() 1600 pc.y = 50 1601 pc.x = 150 1602 pc.width = 100 1603 pc.height = 100 1604 1605 pc.data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1606 pc.labels = ['example1', 'example2', 'example3', 'example4', 'example5', 'example6', 'example7', 1607 'example8', 'example9', 'example10', 'example11', 'example12', 'example13', 'example14', 1608 'example15', 'example16', 'example17', 'example18', 'example19', 'example20', 'example21', 1609 'example22', 'example23', 'example24', 'example25', 'example26', 'example27', 'example28'] 1610 pc.sideLabels = 1 1611 pc.checkLabelOverlap = 1 1612 pc.simpleLabels = 0 1613 1614 1615 pc.slices.strokeWidth=1#0.5 1616 pc.slices[0].fillColor = colors.steelblue 1617 pc.slices[1].fillColor = colors.thistle 1618 pc.slices[2].fillColor = colors.cornflower 1619 pc.slices[3].fillColor = colors.lightsteelblue 1620 pc.slices[4].fillColor = colors.aquamarine 1621 pc.slices[5].fillColor = colors.cadetblue 1622 1623 d.add(pc) 1624 1625 return d 1626 1627def sample8(): 1628 1629 "Case with overlapping labels" 1630 "Labels overlap if they do not belong to adjacent pie slices due to nature of checkLabelOverlap" 1631 1632 d = Drawing(400, 200) 1633 1634 pc = Pie() 1635 pc.y = 50 1636 pc.x = 150 1637 pc.width = 100 1638 pc.height = 100 1639 1640 pc.data = [1, 1, 1, 1, 1, 30, 50, 1, 1, 1, 1, 1, 1, 40,20,10] 1641 pc.labels = ['example1', 'example2', 'example3', 'example4', 'example5', 'example6', 'example7', 1642 'example8', 'example9', 'example10', 'example11', 'example12', 'example13', 'example14', 1643 'example15', 'example16'] 1644 pc.sideLabels = 1 1645 pc.checkLabelOverlap = 1 1646 1647 pc.slices.strokeWidth=1#0.5 1648 pc.slices[0].fillColor = colors.steelblue 1649 pc.slices[1].fillColor = colors.thistle 1650 pc.slices[2].fillColor = colors.cornflower 1651 pc.slices[3].fillColor = colors.lightsteelblue 1652 pc.slices[4].fillColor = colors.aquamarine 1653 pc.slices[5].fillColor = colors.cadetblue 1654 1655 d.add(pc) 1656 1657 return d 1658 1659def sample9(): 1660 1661 "Case with overlapping labels" 1662 "Labels overlap if they do not belong to adjacent pies due to nature of checkLabelOverlap" 1663 1664 d = Drawing(400, 200) 1665 1666 pc = Pie() 1667 pc.x = 125 1668 pc.y = 50 1669 1670 pc.data = [41, 20, 40, 15, 20, 30, 50, 15, 25, 35, 25, 20, 30, 40, 20, 30] 1671 pc.labels = ['example1', 'example2', 'example3', 'example4', 'example5', 'example6', 'example7', 1672 'example8', 'example9', 'example10', 'example11', 'example12', 'example13', 'example14', 1673 'example15', 'example16'] 1674 pc.sideLabels = 1 1675 pc.checkLabelOverlap = 1 1676 1677 pc.width = 100 1678 pc.height = 100 1679 pc.slices.strokeWidth=1#0.5 1680 pc.slices[0].fillColor = colors.steelblue 1681 pc.slices[1].fillColor = colors.thistle 1682 pc.slices[2].fillColor = colors.cornflower 1683 pc.slices[3].fillColor = colors.lightsteelblue 1684 pc.slices[4].fillColor = colors.aquamarine 1685 pc.slices[5].fillColor = colors.cadetblue 1686 1687 d.add(pc) 1688 1689 return d 1690 1691if __name__=='__main__': 1692 """Normally nobody will execute this 1693 1694 It's helpful for reportlab developers to put a 'main' block in to execute 1695 the most recently edited feature. 1696 """ 1697 import sys 1698 from reportlab.graphics import renderPDF 1699 argv = sys.argv[1:] or ['7'] 1700 for a in argv: 1701 name = a if a.startswith('sample') else 'sample%s' % a 1702 drawing = globals()[name]() 1703 renderPDF.drawToFile(drawing, '%s.pdf' % name) 1704 1705 1706 1707