class SubColProperty(PropHolder): dividerLines = 0 _attrMap = AttrMap( minWidth=AttrMapValue(isNumber, desc="minimum width for this subcol"), rpad=AttrMapValue(isNumber, desc="right padding for this subcol"), align=AttrMapValue(OneOf('left', 'right', 'center', 'centre', 'numeric'), desc='alignment in subCol'), fontName=AttrMapValue(isString, desc="Font name of the strings"), fontSize=AttrMapValue(isNumber, desc="Font size of the strings"), leading=AttrMapValue(isNumberOrNone, desc="leading for the strings"), fillColor=AttrMapValue(isColorOrNone, desc="fontColor"), underlines=AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)), SequenceOf(isInstanceOf(Line), emptyOK=0, lo=0, hi=0x7fffffff))), desc="underline definitions"), overlines=AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)), SequenceOf(isInstanceOf(Line), emptyOK=0, lo=0, hi=0x7fffffff))), desc="overline definitions"), dx=AttrMapValue(isNumber, desc="x offset from default position"), dy=AttrMapValue(isNumber, desc="y offset from default position"), vAlign=AttrMapValue(OneOf('top', 'bottom', 'middle'), desc='vertical alignment in the row'), )
class BarChartLabel(PMVLabel): """ An extended Label allowing for nudging, lines visibility etc """ _attrMap = AttrMap( BASE=PMVLabel, lineStrokeWidth=AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"), lineStrokeColor=AttrMapValue(isColorOrNone, desc="Color for a drawn line"), fixedEnd=AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"), fixedStart=AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"), nudge=AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"), boxTarget=AttrMapValue( OneOf('normal', 'anti', 'lo', 'hi', 'mid'), desc="one of ('normal','anti','lo','hi','mid')"), ) def __init__(self): PMVLabel.__init__(self) self.lineStrokeWidth = 0 self.lineStrokeColor = None self.fixedStart = self.fixedEnd = None self.nudge = 0
class LineChartProperties(PropHolder): _attrMap = AttrMap( strokeWidth=AttrMapValue(isNumber, desc='Width of a line.'), strokeColor=AttrMapValue(isColorOrNone, desc='Color of a line or border.'), fillColor=AttrMapValue(isColorOrNone, desc='fill color of a bar.'), strokeDashArray=AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), symbol=AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.', advancedUsage=1), shader=AttrMapValue(None, desc='Shader Class.', advancedUsage=1), filler=AttrMapValue(None, desc='Filler Class.', advancedUsage=1), name=AttrMapValue(isStringOrNone, desc='Name of the line.'), lineStyle=AttrMapValue(NoneOr(OneOf('line', 'joinedLine', 'bar')), desc="What kind of plot this line is", advancedUsage=1), barWidth=AttrMapValue( isNumberOrNone, desc="Percentage of available width to be used for a bar", advancedUsage=1), inFill=AttrMapValue(isBoolean, desc='If true flood fill to x axis', advancedUsage=1), )
def decorate(self, l, L): chart, g, rowNo, colNo, x, y, width, height, x00, y00, x0, y0 = l._callOutInfo L.setText(chart.categoryAxis.categoryNames[colNo]) g.add(L) def __call__(self, l): from copy import deepcopy L = Label() for a, v in self.__dict__.items(): if v is None: v = getattr(l, a, None) setattr(L, a, v) self.decorate(l, L) isOffsetMode = OneOf('high', 'low', 'bar', 'axis') class LabelOffset(PropHolder): _attrMap = AttrMap( posMode=AttrMapValue(isOffsetMode, desc="Where to base +ve offset"), pos=AttrMapValue(isNumber, desc='Value for positive elements'), negMode=AttrMapValue(isOffsetMode, desc="Where to base -ve offset"), neg=AttrMapValue(isNumber, desc='Value for negative elements'), ) def __init__(self): self.posMode = self.negMode = 'axis' self.pos = self.neg = 0 def _getValue(self, chart, val):
class Marker(Widget): '''A polymorphic class of markers''' _attrMap = AttrMap(BASE=Widget, kind = AttrMapValue( OneOf(None, 'Square', 'Diamond', 'Circle', 'Cross', 'Triangle', 'StarSix', 'Pentagon', 'Hexagon', 'Heptagon', 'Octagon', 'StarFive', 'FilledSquare', 'FilledCircle', 'FilledDiamond', 'FilledCross', 'FilledTriangle','FilledStarSix', 'FilledPentagon', 'FilledHexagon', 'FilledHeptagon', 'FilledOctagon', 'FilledStarFive', 'Smiley','ArrowHead', 'FilledArrowHead'), desc='marker type name'), size = AttrMapValue(isNumber,desc='marker size'), x = AttrMapValue(isNumber,desc='marker x coordinate'), y = AttrMapValue(isNumber,desc='marker y coordinate'), dx = AttrMapValue(isNumber,desc='marker x coordinate adjustment'), dy = AttrMapValue(isNumber,desc='marker y coordinate adjustment'), angle = AttrMapValue(isNumber,desc='marker rotation'), fillColor = AttrMapValue(isColorOrNone, desc='marker fill colour'), strokeColor = AttrMapValue(isColorOrNone, desc='marker stroke colour'), strokeWidth = AttrMapValue(isNumber, desc='marker stroke width'), arrowBarbDx = AttrMapValue(isNumber, desc='arrow only the delta x for the barbs'), arrowHeight = AttrMapValue(isNumber, desc='arrow only height'), ) def __init__(self,*args,**kw): self.setProperties(kw) self._setKeywords( kind = None, strokeColor = black, strokeWidth = 0.1, fillColor = None, size = 5, x = 0, y = 0, dx = 0, dy = 0, angle = 0, arrowBarbDx = -1.25, arrowHeight = 1.875, ) def clone(self): return new.instance(self.__class__,self.__dict__.copy()) def _Smiley(self): x, y = self.x+self.dx, self.y+self.dy d = self.size/2.0 s = SmileyFace() s.fillColor = self.fillColor s.strokeWidth = self.strokeWidth s.strokeColor = self.strokeColor s.x = x-d s.y = y-d s.size = d*2 return s def _Square(self): x, y = self.x+self.dx, self.y+self.dy d = self.size/2.0 s = Rect(x-d,y-d,2*d,2*d,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) return s def _Diamond(self): d = self.size/2.0 return self._doPolygon((-d,0,0,d,d,0,0,-d)) def _Circle(self): x, y = self.x+self.dx, self.y+self.dy s = Circle(x,y,self.size/2.0,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) return s def _Cross(self): x, y = self.x+self.dx, self.y+self.dy s = float(self.size) h, s = s/2, s/6 return self._doPolygon((-s,-h,-s,-s,-h,-s,-h,s,-s,s,-s,h,s,h,s,s,h,s,h,-s,s,-s,s,-h)) def _Triangle(self): x, y = self.x+self.dx, self.y+self.dy r = float(self.size)/2 c = 30*_toradians s = sin(30*_toradians)*r c = cos(c)*r return self._doPolygon((0,r,-c,-s,c,-s)) def _StarSix(self): r = float(self.size)/2 c = 30*_toradians s = sin(c)*r c = cos(c)*r z = s/2 g = c/2 return self._doPolygon((0,r,-z,s,-c,s,-s,0,-c,-s,-z,-s,0,-r,z,-s,c,-s,s,0,c,s,z,s)) def _StarFive(self): R = float(self.size)/2 r = R*sin(18*_toradians)/cos(36*_toradians) P = [] angle = 90 for i in xrange(5): for radius in R, r: theta = angle*_toradians P.append(radius*cos(theta)) P.append(radius*sin(theta)) angle = angle + 36 return self._doPolygon(P) def _Pentagon(self): return self._doNgon(5) def _Hexagon(self): return self._doNgon(6) def _Heptagon(self): return self._doNgon(7) def _Octagon(self): return self._doNgon(8) def _ArrowHead(self): s = self.size h = self.arrowHeight b = self.arrowBarbDx return self._doPolygon((0,0,b,-h,s,0,b,h)) def _doPolygon(self,P): x, y = self.x+self.dx, self.y+self.dy if x or y: P = map(lambda i,P=P,A=[x,y]: P[i] + A[i&1], range(len(P))) return Polygon(P, strokeWidth =self.strokeWidth, strokeColor=self.strokeColor, fillColor=self.fillColor) def _doFill(self): old = self.fillColor if old is None: self.fillColor = self.strokeColor r = (self.kind and getattr(self,'_'+self.kind[6:]) or Group)() self.fillColor = old return r def _doNgon(self,n): P = [] size = float(self.size)/2 for i in xrange(n): r = (2.*i/n+0.5)*pi P.append(size*cos(r)) P.append(size*sin(r)) return self._doPolygon(P) _FilledCircle = _doFill _FilledSquare = _doFill _FilledDiamond = _doFill _FilledCross = _doFill _FilledTriangle = _doFill _FilledStarSix = _doFill _FilledPentagon = _doFill _FilledHexagon = _doFill _FilledHeptagon = _doFill _FilledOctagon = _doFill _FilledStarFive = _doFill _FilledArrowHead = _doFill def draw(self): if self.kind: m = getattr(self,'_'+self.kind) if self.angle: _x, _dx, _y, _dy = self.x, self.dx, self.y, self.dy self.x, self.dx, self.y, self.dy = 0,0,0,0 try: m = m() finally: self.x, self.dx, self.y, self.dy = _x, _dx, _y, _dy if not isinstance(m,Group): _m, m = m, Group() m.add(_m) if self.angle: m.rotate(self.angle) x, y = _x+_dx, _y+_dy if x or y: m.shift(x,y) else: m = m() else: m = Group() return m
class Doughnut(AbstractPieChart): _attrMap = AttrMap( x=AttrMapValue(isNumber, desc='X position of the chart within its container.'), y=AttrMapValue(isNumber, desc='Y position of the chart within its container.'), width=AttrMapValue( isNumber, desc='width of doughnut bounding box. Need not be same as width.'), height=AttrMapValue( isNumber, desc='height of doughnut bounding box. Need not be same as height.' ), data=AttrMapValue( None, desc='list of numbers defining sector sizes; need not sum to 1'), labels=AttrMapValue( isListOfStringsOrNone, desc="optional list of labels to use for each data point"), startAngle=AttrMapValue( isNumber, desc="angle of first slice; like the compass, 0 is due North"), direction=AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), slices=AttrMapValue(None, desc="collection of sector descriptor objects"), simpleLabels=AttrMapValue( isBoolean, desc="If true(default) use String not super duper WedgeLabel"), # advanced usage checkLabelOverlap=AttrMapValue( isBoolean, desc= "If true check and attempt to fix\n standard label overlaps(default off)", advancedUsage=1), sideLabels=AttrMapValue( isBoolean, desc= "If true attempt to make chart with labels along side and pointers", advancedUsage=1)) def __init__(self): self.x = 0 self.y = 0 self.width = 100 self.height = 100 self.data = [1, 1] self.labels = None # or list of strings self.startAngle = 90 self.direction = "clockwise" self.simpleLabels = 1 self.checkLabelOverlap = 0 self.sideLabels = 0 self.slices = TypedPropertyCollection(SectorProperties) self.slices[0].fillColor = colors.darkcyan self.slices[1].fillColor = colors.blueviolet self.slices[2].fillColor = colors.blue self.slices[3].fillColor = colors.cyan self.slices[4].fillColor = colors.pink self.slices[5].fillColor = colors.magenta self.slices[6].fillColor = colors.yellow def demo(self): d = Drawing(200, 100) dn = Doughnut() dn.x = 50 dn.y = 10 dn.width = 100 dn.height = 80 dn.data = [10, 20, 30, 40, 50, 60] dn.labels = ['a', 'b', 'c', 'd', 'e', 'f'] dn.slices.strokeWidth = 0.5 dn.slices[3].popout = 10 dn.slices[3].strokeWidth = 2 dn.slices[3].strokeDashArray = [2, 2] dn.slices[3].labelRadius = 1.75 dn.slices[3].fontColor = colors.red dn.slices[0].fillColor = colors.darkcyan dn.slices[1].fillColor = colors.blueviolet dn.slices[2].fillColor = colors.blue dn.slices[3].fillColor = colors.cyan dn.slices[4].fillColor = colors.aquamarine dn.slices[5].fillColor = colors.cadetblue dn.slices[6].fillColor = colors.lightcoral d.add(dn) return d def normalizeData(self, data=None): from operator import add sum = float(reduce(add, data, 0)) return abs(sum) >= 1e-8 and map(lambda x, f=360. / sum: f * x, data) or len(data) * [0] def makeSectors(self): # normalize slice data if type(self.data) in (ListType, TupleType) and type( self.data[0]) in (ListType, TupleType): #it's a nested list, more than one sequence normData = [] n = [] for l in self.data: t = self.normalizeData(l) normData.append(t) n.append(len(t)) self._seriesCount = max(n) else: normData = self.normalizeData(self.data) n = len(normData) self._seriesCount = n #labels checkLabelOverlap = self.checkLabelOverlap L = [] L_add = L.append if self.labels is None: labels = [] if type(n) not in (ListType, TupleType): labels = [''] * n else: for m in n: labels = list(labels) + [''] * m else: labels = self.labels #there's no point in raising errors for less than enough labels if #we silently create all for the extreme case of no labels. if type(n) not in (ListType, TupleType): i = n - len(labels) if i > 0: labels = list(labels) + [''] * i else: tlab = 0 for m in n: tlab += m i = tlab - len(labels) if i > 0: labels = list(labels) + [''] * i xradius = self.width / 2.0 yradius = self.height / 2.0 centerx = self.x + xradius centery = self.y + yradius if self.direction == "anticlockwise": whichWay = 1 else: whichWay = -1 g = Group() startAngle = self.startAngle #% 360 styleCount = len(self.slices) if type(self.data[0]) in (ListType, TupleType): #multi-series doughnut iradius = (self.height / 5.0) / len(self.data) for sn, series in enumerate(normData): for i, angle in enumerate(series): endAngle = (startAngle + (angle * whichWay)) #% 360 if abs(startAngle - endAngle) < 1e-5: startAngle = endAngle continue if startAngle < endAngle: a1 = startAngle a2 = endAngle else: a1 = endAngle a2 = startAngle startAngle = endAngle #if we didn't use %stylecount here we'd end up with the later sectors #all having the default style sectorStyle = self.slices[i % styleCount] # is it a popout? cx, cy = centerx, centery if sectorStyle.popout != 0: # pop out the sector averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 popdistance = sectorStyle.popout cx = centerx + popdistance * cos(aveAngleRadians) cy = centery + popdistance * sin(aveAngleRadians) if type(n) in (ListType, TupleType): theSector = Wedge( cx, cy, xradius + (sn * iradius) - iradius, a1, a2, yradius=yradius + (sn * iradius) - iradius, radius1=yradius + (sn * iradius) - (2 * iradius)) else: theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=iradius) theSector.fillColor = sectorStyle.fillColor theSector.strokeColor = sectorStyle.strokeColor theSector.strokeWidth = sectorStyle.strokeWidth theSector.strokeDashArray = sectorStyle.strokeDashArray g.add(theSector) if sn == 0: text = self.getSeriesName(i, '') if text: averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 labelRadius = sectorStyle.labelRadius rx = xradius * labelRadius ry = yradius * labelRadius labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) l = _addWedgeLabel(self, text, averageAngle, labelX, labelY, sectorStyle) if checkLabelOverlap: l._origdata = { 'x': labelX, 'y': labelY, 'angle': averageAngle, 'rx': rx, 'ry': ry, 'cx': cx, 'cy': cy, 'bounds': l.getBounds(), } L_add(l) else: #single series doughnut iradius = self.height / 5.0 for i, angle in enumerate(normData): endAngle = (startAngle + (angle * whichWay)) #% 360 if abs(startAngle - endAngle) < 1e-5: startAngle = endAngle continue if startAngle < endAngle: a1 = startAngle a2 = endAngle else: a1 = endAngle a2 = startAngle startAngle = endAngle #if we didn't use %stylecount here we'd end up with the later sectors #all having the default style sectorStyle = self.slices[i % styleCount] # is it a popout? cx, cy = centerx, centery if sectorStyle.popout != 0: # pop out the sector averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 popdistance = sectorStyle.popout cx = centerx + popdistance * cos(aveAngleRadians) cy = centery + popdistance * sin(aveAngleRadians) if n > 1: theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=iradius) elif n == 1: theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, iradius=iradius) theSector.fillColor = sectorStyle.fillColor theSector.strokeColor = sectorStyle.strokeColor theSector.strokeWidth = sectorStyle.strokeWidth theSector.strokeDashArray = sectorStyle.strokeDashArray g.add(theSector) # now draw a label if labels[i] != "": averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 labelRadius = sectorStyle.labelRadius labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) rx = xradius * labelRadius ry = yradius * labelRadius l = _addWedgeLabel(self, labels[i], averageAngle, labelX, labelY, sectorStyle) if checkLabelOverlap: l._origdata = { 'x': labelX, 'y': labelY, 'angle': averageAngle, 'rx': rx, 'ry': ry, 'cx': cx, 'cy': cy, 'bounds': l.getBounds(), } L_add(l) if checkLabelOverlap and L: fixLabelOverlaps(L) for l in L: g.add(l) return g def draw(self): g = Group() g.add(self.makeSectors()) return g
class Doughnut(AbstractPieChart): _attrMap = AttrMap( x=AttrMapValue(isNumber, desc='X position of the chart within its container.'), y=AttrMapValue(isNumber, desc='Y position of the chart within its container.'), width=AttrMapValue( isNumber, desc='width of doughnut bounding box. Need not be same as width.'), height=AttrMapValue( isNumber, desc='height of doughnut bounding box. Need not be same as height.' ), data=AttrMapValue( EitherOr((isListOfNoneOrNumber, isListOfListOfNoneOrNumber)), desc='list of numbers defining sector sizes; need not sum to 1'), labels=AttrMapValue( isListOfStringsOrNone, desc="optional list of labels to use for each data point"), startAngle=AttrMapValue( isNumber, desc="angle of first slice; like the compass, 0 is due North"), direction=AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), slices=AttrMapValue(None, desc="collection of sector descriptor objects"), simpleLabels=AttrMapValue( isBoolean, desc="If true(default) use String not super duper WedgeLabel"), # advanced usage checkLabelOverlap=AttrMapValue( isBoolean, desc= "If true check and attempt to fix\n standard label overlaps(default off)", advancedUsage=1), sideLabels=AttrMapValue( isBoolean, desc= "If true attempt to make chart with labels along side and pointers", advancedUsage=1), innerRadiusFraction=AttrMapValue( isNumberOrNone, desc= 'None or the fraction of the radius to be used as the inner hole.\nIf not a suitable default will be used.' ), ) def __init__(self): self.x = 0 self.y = 0 self.width = 100 self.height = 100 self.data = [1, 1] self.labels = None # or list of strings self.startAngle = 90 self.direction = "clockwise" self.simpleLabels = 1 self.checkLabelOverlap = 0 self.sideLabels = 0 self.innerRadiusFraction = None self.slices = TypedPropertyCollection(SectorProperties) self.slices[0].fillColor = colors.darkcyan self.slices[1].fillColor = colors.blueviolet self.slices[2].fillColor = colors.blue self.slices[3].fillColor = colors.cyan self.slices[4].fillColor = colors.pink self.slices[5].fillColor = colors.magenta self.slices[6].fillColor = colors.yellow def demo(self): d = Drawing(200, 100) dn = Doughnut() dn.x = 50 dn.y = 10 dn.width = 100 dn.height = 80 dn.data = [10, 20, 30, 40, 50, 60] dn.labels = ['a', 'b', 'c', 'd', 'e', 'f'] dn.slices.strokeWidth = 0.5 dn.slices[3].popout = 10 dn.slices[3].strokeWidth = 2 dn.slices[3].strokeDashArray = [2, 2] dn.slices[3].labelRadius = 1.75 dn.slices[3].fontColor = colors.red dn.slices[0].fillColor = colors.darkcyan dn.slices[1].fillColor = colors.blueviolet dn.slices[2].fillColor = colors.blue dn.slices[3].fillColor = colors.cyan dn.slices[4].fillColor = colors.aquamarine dn.slices[5].fillColor = colors.cadetblue dn.slices[6].fillColor = colors.lightcoral d.add(dn) return d def normalizeData(self, data=None): from operator import add sum = float(reduce(add, data, 0)) return abs(sum) >= 1e-8 and list( map(lambda x, f=360. / sum: f * x, data)) or len(data) * [0] def makeSectors(self): # normalize slice data data = self.data multi = isListOfListOfNoneOrNumber(data) if multi: #it's a nested list, more than one sequence normData = [] n = [] for l in data: t = self.normalizeData(l) normData.append(t) n.append(len(t)) self._seriesCount = max(n) else: normData = self.normalizeData(data) n = len(normData) self._seriesCount = n #labels checkLabelOverlap = self.checkLabelOverlap L = [] L_add = L.append labels = self.labels if labels is None: labels = [] if not multi: labels = [''] * n else: for m in n: labels = list(labels) + [''] * m else: #there's no point in raising errors for less than enough labels if #we silently create all for the extreme case of no labels. if not multi: i = n - len(labels) if i > 0: labels = list(labels) + [''] * i else: tlab = 0 for m in n: tlab += m i = tlab - len(labels) if i > 0: labels = list(labels) + [''] * i self.labels = labels xradius = self.width / 2.0 yradius = self.height / 2.0 centerx = self.x + xradius centery = self.y + yradius if self.direction == "anticlockwise": whichWay = 1 else: whichWay = -1 g = Group() startAngle = self.startAngle #% 360 styleCount = len(self.slices) irf = self.innerRadiusFraction if multi: #multi-series doughnut ndata = len(data) if irf is None: yir = (yradius / 2.5) / ndata xir = (xradius / 2.5) / ndata else: yir = yradius * irf xir = xradius * irf ydr = (yradius - yir) / ndata xdr = (xradius - xir) / ndata for sn, series in enumerate(normData): for i, angle in enumerate(series): endAngle = (startAngle + (angle * whichWay)) #% 360 aa = abs(startAngle - endAngle) if aa < 1e-5: startAngle = endAngle continue if startAngle < endAngle: a1 = startAngle a2 = endAngle else: a1 = endAngle a2 = startAngle startAngle = endAngle #if we didn't use %stylecount here we'd end up with the later sectors #all having the default style sectorStyle = self.slices[sn, i % styleCount] # is it a popout? cx, cy = centerx, centery if sectorStyle.popout != 0: # pop out the sector averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 popdistance = sectorStyle.popout cx = centerx + popdistance * cos(aveAngleRadians) cy = centery + popdistance * sin(aveAngleRadians) yr1 = yir + sn * ydr yr = yr1 + ydr xr1 = xir + sn * xdr xr = xr1 + xdr if len(series) > 1: theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1) else: theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True) theSector.fillColor = sectorStyle.fillColor theSector.strokeColor = sectorStyle.strokeColor theSector.strokeWidth = sectorStyle.strokeWidth theSector.strokeDashArray = sectorStyle.strokeDashArray shader = sectorStyle.shadingKind if shader: nshades = aa / float(sectorStyle.shadingAngle) if nshades > 1: shader = colors.Whiter if shader == 'lighten' else colors.Blacker nshades = 1 + int(nshades) shadingAmount = 1 - sectorStyle.shadingAmount if sectorStyle.shadingDirection == 'normal': dsh = (1 - shadingAmount) / float(nshades - 1) shf1 = shadingAmount else: dsh = (shadingAmount - 1) / float(nshades - 1) shf1 = 1 shda = (a2 - a1) / float(nshades) shsc = sectorStyle.fillColor theSector.fillColor = None for ish in range(nshades): sha1 = a1 + ish * shda sha2 = a1 + (ish + 1) * shda shc = shader(shsc, shf1 + dsh * ish) if len(series) > 1: shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1) else: shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True) shSector.fillColor = shc shSector.strokeColor = None shSector.strokeWidth = 0 g.add(shSector) g.add(theSector) if sn == 0 and sectorStyle.visible and sectorStyle.label_visible: text = self.getSeriesName(i, '') if text: averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 labelRadius = sectorStyle.labelRadius rx = xradius * labelRadius ry = yradius * labelRadius labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) l = _addWedgeLabel(self, text, averageAngle, labelX, labelY, sectorStyle) if checkLabelOverlap: l._origdata = { 'x': labelX, 'y': labelY, 'angle': averageAngle, 'rx': rx, 'ry': ry, 'cx': cx, 'cy': cy, 'bounds': l.getBounds(), } L_add(l) else: #single series doughnut if irf is None: yir = yradius / 2.5 xir = xradius / 2.5 else: yir = yradius * irf xir = xradius * irf for i, angle in enumerate(normData): endAngle = (startAngle + (angle * whichWay)) #% 360 aa = abs(startAngle - endAngle) if aa < 1e-5: startAngle = endAngle continue if startAngle < endAngle: a1 = startAngle a2 = endAngle else: a1 = endAngle a2 = startAngle startAngle = endAngle #if we didn't use %stylecount here we'd end up with the later sectors #all having the default style sectorStyle = self.slices[i % styleCount] # is it a popout? cx, cy = centerx, centery if sectorStyle.popout != 0: # pop out the sector averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 popdistance = sectorStyle.popout cx = centerx + popdistance * cos(aveAngleRadians) cy = centery + popdistance * sin(aveAngleRadians) if n > 1: theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir) elif n == 1: theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir, annular=True) theSector.fillColor = sectorStyle.fillColor theSector.strokeColor = sectorStyle.strokeColor theSector.strokeWidth = sectorStyle.strokeWidth theSector.strokeDashArray = sectorStyle.strokeDashArray shader = sectorStyle.shadingKind if shader: nshades = aa / float(sectorStyle.shadingAngle) if nshades > 1: shader = colors.Whiter if shader == 'lighten' else colors.Blacker nshades = 1 + int(nshades) shadingAmount = 1 - sectorStyle.shadingAmount if sectorStyle.shadingDirection == 'normal': dsh = (1 - shadingAmount) / float(nshades - 1) shf1 = shadingAmount else: dsh = (shadingAmount - 1) / float(nshades - 1) shf1 = 1 shda = (a2 - a1) / float(nshades) shsc = sectorStyle.fillColor theSector.fillColor = None for ish in range(nshades): sha1 = a1 + ish * shda sha2 = a1 + (ish + 1) * shda shc = shader(shsc, shf1 + dsh * ish) if n > 1: shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir) elif n == 1: shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir, annular=True) shSector.fillColor = shc shSector.strokeColor = None shSector.strokeWidth = 0 g.add(shSector) g.add(theSector) # now draw a label if labels[ i] and sectorStyle.visible and sectorStyle.label_visible: averageAngle = (a1 + a2) / 2.0 aveAngleRadians = averageAngle * pi / 180.0 labelRadius = sectorStyle.labelRadius labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) rx = xradius * labelRadius ry = yradius * labelRadius l = _addWedgeLabel(self, labels[i], averageAngle, labelX, labelY, sectorStyle) if checkLabelOverlap: l._origdata = { 'x': labelX, 'y': labelY, 'angle': averageAngle, 'rx': rx, 'ry': ry, 'cx': cx, 'cy': cy, 'bounds': l.getBounds(), } L_add(l) if checkLabelOverlap and L: fixLabelOverlaps(L) for l in L: g.add(l) return g def draw(self): g = Group() g.add(self.makeSectors()) return g
class Legend(Widget): """A simple legend containing rectangular swatches and strings. The swatches are filled rectangles whenever the respective color object in 'colorNamePairs' is a subclass of Color in reportlab.lib.colors. Otherwise the object passed instead is assumed to have 'x', 'y', 'width' and 'height' attributes. A legend then tries to set them or catches any error. This lets you plug-in any widget you like as a replacement for the default rectangular swatches. Strings can be nicely aligned left or right to the swatches. """ _attrMap = AttrMap( x=AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"), y=AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"), deltax=AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"), deltay=AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"), dxTextSpace=AttrMapValue( isNumber, desc="Distance between swatch rectangle and text"), autoXPadding=AttrMapValue( isNumber, desc="x Padding between columns if deltax=None", advancedUsage=1), autoYPadding=AttrMapValue(isNumber, desc="y Padding between rows if deltay=None", advancedUsage=1), yGap=AttrMapValue(isNumber, desc="Additional gap between rows", advancedUsage=1), dx=AttrMapValue(isNumber, desc="Width of swatch rectangle"), dy=AttrMapValue(isNumber, desc="Height of swatch rectangle"), columnMaximum=AttrMapValue(isNumber, desc="Max. number of items per column"), alignment=AttrMapValue( OneOf("left", "right"), desc="Alignment of text with respect to swatches"), colorNamePairs=AttrMapValue( None, desc="List of color/name tuples (color can also be widget)"), fontName=AttrMapValue(isString, desc="Font name of the strings"), fontSize=AttrMapValue(isNumber, desc="Font size of the strings"), fillColor=AttrMapValue(isColorOrNone, desc="swatches filling color"), strokeColor=AttrMapValue(isColorOrNone, desc="Border color of the swatches"), strokeWidth=AttrMapValue( isNumber, desc="Width of the border color of the swatches"), swatchMarker=AttrMapValue( NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ...", advancedUsage=1), callout=AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))", advancedUsage=1), boxAnchor=AttrMapValue(isBoxAnchor, 'Anchor point for the legend area'), variColumn=AttrMapValue( isBoolean, 'If true column widths may vary (default is false)', advancedUsage=1), dividerLines=AttrMapValue( OneOf(0, 1, 2, 3, 4, 5, 6, 7), 'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom', advancedUsage=1), dividerWidth=AttrMapValue(isNumber, desc="dividerLines width", advancedUsage=1), dividerColor=AttrMapValue(isColorOrNone, desc="dividerLines color", advancedUsage=1), dividerDashArray=AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.', advancedUsage=1), dividerOffsX=AttrMapValue(SequenceOf(isNumber, emptyOK=0, lo=2, hi=2), desc='divider lines X offsets', advancedUsage=1), dividerOffsY=AttrMapValue(isNumber, desc="dividerLines Y offset", advancedUsage=1), colEndCallout=AttrMapValue( None, desc="a user callout(self,g, x, xt, y,width, lWidth)", advancedUsage=1), subCols=AttrMapValue(None, desc="subColumn properties"), swatchCallout=AttrMapValue( None, desc="a user swatch callout(self,g,x,y,i,(col,name),swatch)", advancedUsage=1), ) def __init__(self): # Upper-left reference point. self.x = 0 self.y = 0 # Alginment of text with respect to swatches. self.alignment = "left" # x- and y-distances between neighbouring swatches. self.deltax = 75 self.deltay = 20 self.autoXPadding = 5 self.autoYPadding = 2 # Size of swatch rectangle. self.dx = 10 self.dy = 10 # Distance between swatch rectangle and text. self.dxTextSpace = 10 # Max. number of items per column. self.columnMaximum = 3 # Color/name pairs. self.colorNamePairs = [(colors.red, "red"), (colors.blue, "blue"), (colors.green, "green"), (colors.pink, "pink"), (colors.yellow, "yellow")] # Font name and size of the labels. self.fontName = STATE_DEFAULTS['fontName'] self.fontSize = STATE_DEFAULTS['fontSize'] self.fillColor = STATE_DEFAULTS['fillColor'] self.strokeColor = STATE_DEFAULTS['strokeColor'] self.strokeWidth = STATE_DEFAULTS['strokeWidth'] self.swatchMarker = None self.boxAnchor = 'nw' self.yGap = 0 self.variColumn = 0 self.dividerLines = 0 self.dividerWidth = 0.5 self.dividerDashArray = None self.dividerColor = colors.black self.dividerOffsX = (0, 0) self.dividerOffsY = 0 self.colEndCallout = None self._init_subCols() def _init_subCols(self): sc = self.subCols = TypedPropertyCollection(SubColProperty) sc.rpad = 1 sc.minWidth = 0 sc.align = 'right' sc[0].align = 'left' def _getChartStyleName(self, chart): for a in 'lines', 'bars', 'slices', 'strands': if hasattr(chart, a): return a return None def _getChartStyle(self, chart): return getattr(chart, self._getChartStyleName(chart), None) def _getTexts(self, colorNamePairs): if not isAuto(colorNamePairs): texts = [_getStr(p[1]) for p in colorNamePairs] else: chart = getattr(colorNamePairs, 'chart', getattr(colorNamePairs, 'obj', None)) texts = [ chart.getSeriesName(i, 'series %d' % i) for i in xrange(chart._seriesCount) ] return texts def _calculateMaxBoundaries(self, colorNamePairs): "Calculate the maximum width of some given strings." fontName = self.fontName fontSize = self.fontSize subCols = self.subCols M = [ _getWidths(i, m, fontName, fontSize, subCols) for i, m in enumerate(self._getTexts(colorNamePairs)) ] if not M: return [0, 0] n = max([len(m) for m in M]) if self.variColumn: columnMaximum = self.columnMaximum return [ _transMax(n, M[r:r + columnMaximum]) for r in xrange(0, len(M), self.columnMaximum) ] else: return _transMax(n, M) def _calcHeight(self): dy = self.dy yGap = self.yGap thisy = upperlefty = self.y - dy fontSize = self.fontSize ascent = getFont(self.fontName).face.ascent / 1000. if ascent == 0: ascent = 0.718 # default (from helvetica) ascent *= fontSize leading = fontSize * 1.2 deltay = self.deltay if not deltay: deltay = max(dy, leading) + self.autoYPadding columnCount = 0 count = 0 lowy = upperlefty lim = self.columnMaximum - 1 for name in self._getTexts(self.colorNamePairs): y0 = thisy + (dy - ascent) * 0.5 y = y0 - _getLineCount(name) * leading leadingMove = 2 * y0 - y - thisy newy = thisy - max(deltay, leadingMove) - yGap lowy = min(y, newy, lowy) if count == lim: count = 0 thisy = upperlefty columnCount = columnCount + 1 else: thisy = newy count = count + 1 return upperlefty - lowy def _defaultSwatch(self, x, thisy, dx, dy, fillColor, strokeWidth, strokeColor): return Rect( x, thisy, dx, dy, fillColor=fillColor, strokeColor=strokeColor, strokeWidth=strokeWidth, ) def draw(self): colorNamePairs = self.colorNamePairs autoCP = isAuto(colorNamePairs) if autoCP: chart = getattr(colorNamePairs, 'chart', getattr(colorNamePairs, 'obj', None)) swatchMarker = None autoCP = Auto(obj=chart) n = chart._seriesCount chartTexts = self._getTexts(colorNamePairs) else: swatchMarker = getattr(self, 'swatchMarker', None) if isAuto(swatchMarker): chart = getattr(swatchMarker, 'chart', getattr(swatchMarker, 'obj', None)) swatchMarker = Auto(obj=chart) n = len(colorNamePairs) dx = self.dx dy = self.dy alignment = self.alignment columnMaximum = self.columnMaximum deltax = self.deltax deltay = self.deltay dxTextSpace = self.dxTextSpace fontName = self.fontName fontSize = self.fontSize fillColor = self.fillColor strokeWidth = self.strokeWidth strokeColor = self.strokeColor subCols = self.subCols leading = fontSize * 1.2 yGap = self.yGap if not deltay: deltay = max(dy, leading) + self.autoYPadding ba = self.boxAnchor maxWidth = self._calculateMaxBoundaries(colorNamePairs) nCols = int((n + columnMaximum - 1) / (columnMaximum * 1.0)) xW = dx + dxTextSpace + self.autoXPadding variColumn = self.variColumn if variColumn: width = reduce(operator.add, [m[-1] for m in maxWidth], 0) + xW * nCols else: deltax = max(maxWidth[-1] + xW, deltax) width = maxWidth[-1] + nCols * deltax maxWidth = nCols * [maxWidth] thisx = self.x thisy = self.y - self.dy if ba not in ('ne', 'n', 'nw', 'autoy'): height = self._calcHeight() if ba in ('e', 'c', 'w'): thisy += height / 2. else: thisy += height if ba not in ('nw', 'w', 'sw', 'autox'): if ba in ('n', 'c', 's'): thisx -= width / 2 else: thisx -= width upperlefty = thisy g = Group() ascent = getFont(fontName).face.ascent / 1000. if ascent == 0: ascent = 0.718 # default (from helvetica) ascent *= fontSize # normalize lim = columnMaximum - 1 callout = getattr(self, 'callout', None) scallout = getattr(self, 'swatchCallout', None) dividerLines = self.dividerLines if dividerLines: dividerWidth = self.dividerWidth dividerColor = self.dividerColor dividerDashArray = self.dividerDashArray dividerOffsX = self.dividerOffsX dividerOffsY = self.dividerOffsY for i in xrange(n): if autoCP: col = autoCP col.index = i name = chartTexts[i] else: col, name = colorNamePairs[i] if isAuto(swatchMarker): col = swatchMarker col.index = i if isAuto(name): name = getattr(swatchMarker, 'chart', getattr(swatchMarker, 'obj', None)).getSeriesName( i, 'series %d' % i) T = _getLines(name) S = [] aS = S.append j = int(i / (columnMaximum * 1.0)) jOffs = maxWidth[j] # thisy+dy/2 = y+leading/2 y = y0 = thisy + (dy - ascent) * 0.5 if callout: callout(self, g, thisx, y, (col, name)) if alignment == "left": x = thisx xn = thisx + jOffs[-1] + dxTextSpace elif alignment == "right": x = thisx + dx + dxTextSpace xn = thisx else: raise ValueError, "bad alignment" if not isSeqType(name): T = [T] yd = y for k, lines in enumerate(T): y = y0 kk = k * 2 x1 = x + jOffs[kk] x2 = x + jOffs[kk + 1] sc = subCols[k, i] anchor = sc.align fN = getattr(sc, 'fontName', fontName) fS = getattr(sc, 'fontSize', fontSize) fC = getattr(sc, 'fillColor', fillColor) fL = getattr(sc, 'leading', 1.2 * fontSize) if fN == fontName: fA = (ascent * fS) / fontSize else: fA = getFont(fontName).face.ascent / 1000. if fA == 0: fA = 0.718 fA *= fS if anchor == 'left': anchor = 'start' xoffs = x1 elif anchor == 'right': anchor = 'end' xoffs = x2 elif anchor == 'numeric': xoffs = x2 else: anchor = 'middle' xoffs = 0.5 * (x1 + x2) for t in lines: aS( String(xoffs, y, t, fontName=fN, fontSize=fS, fillColor=fC, textAnchor=anchor)) y -= fL yd = min(yd, y) y += fL for iy, a in ((y - max(fL - fA, 0), 'underlines'), (y + fA, 'overlines')): il = getattr(sc, a, None) if il: if not isinstance(il, (tuple, list)): il = (il, ) for l in il: l = copy.copy(l) l.y1 += iy l.y2 += iy l.x1 += x1 l.x2 += x2 aS(l) x = xn y = yd leadingMove = 2 * y0 - y - thisy if dividerLines: xd = thisx + dx + dxTextSpace + jOffs[-1] + dividerOffsX[1] yd = thisy + dy * 0.5 + dividerOffsY if ((dividerLines & 1) and i % columnMaximum) or ( (dividerLines & 2) and not i % columnMaximum): g.add( Line(thisx + dividerOffsX[0], yd, xd, yd, strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) if (dividerLines & 4) and (i % columnMaximum == lim or i == (n - 1)): yd -= max(deltay, leadingMove) + yGap g.add( Line(thisx + dividerOffsX[0], yd, xd, yd, strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) # Make a 'normal' color swatch... if isAuto(col): chart = getattr(col, 'chart', getattr(col, 'obj', None)) c = chart.makeSwatchSample(getattr(col, 'index', i), x, thisy, dx, dy) elif isinstance(col, colors.Color): if isSymbol(swatchMarker): c = uSymbol2Symbol(swatchMarker, x + dx / 2., thisy + dy / 2., col) else: c = self._defaultSwatch(x, thisy, dx, dy, fillColor=col, strokeWidth=strokeWidth, strokeColor=strokeColor) elif col is not None: try: c = copy.deepcopy(col) c.x = x c.y = thisy c.width = dx c.height = dy except: c = None else: c = None if c: g.add(c) if scallout: scallout(self, g, thisx, y0, i, (col, name), c) for s in S: g.add(s) if self.colEndCallout and (i % columnMaximum == lim or i == (n - 1)): if alignment == "left": xt = thisx else: xt = thisx + dx + dxTextSpace yd = thisy + dy * 0.5 + dividerOffsY - ( max(deltay, leadingMove) + yGap) self.colEndCallout(self, g, thisx, xt, yd, jOffs[-1], jOffs[-1] + dx + dxTextSpace) if i % columnMaximum == lim: if variColumn: thisx += jOffs[-1] + xW else: thisx = thisx + deltax thisy = upperlefty else: thisy = thisy - max(deltay, leadingMove) - yGap return g def demo(self): "Make sample legend." d = Drawing(200, 100) legend = Legend() legend.alignment = 'left' legend.x = 0 legend.y = 100 legend.dxTextSpace = 5 items = 'red green blue yellow pink black white'.split() items = map(lambda i: (getattr(colors, i), i), items) legend.colorNamePairs = items d.add(legend, 'legend') return d
class Grid(Widget): """This makes a rectangular grid of equidistant stripes. The grid contains an outer border rectangle, and stripes inside which can be drawn with lines and/or as solid tiles. The drawing order is: outer rectangle, then lines and tiles. The stripes' width is indicated as 'delta'. The sequence of stripes can have an offset named 'delta0'. Both values need to be positive! """ _attrMap = AttrMap( x=AttrMapValue(isNumber, desc="The grid's lower-left x position."), y=AttrMapValue(isNumber, desc="The grid's lower-left y position."), width=AttrMapValue(isNumber, desc="The grid's width."), height=AttrMapValue(isNumber, desc="The grid's height."), orientation=AttrMapValue( OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'), useLines=AttrMapValue( OneOf((0, 1)), desc='Determines if stripes are drawn with lines.'), useRects=AttrMapValue( OneOf((0, 1)), desc='Determines if stripes are drawn with solid rectangles.'), delta=AttrMapValue(isNumber, desc='Determines the width/height of the stripes.'), delta0=AttrMapValue( isNumber, desc='Determines the stripes initial width/height offset.'), deltaSteps=AttrMapValue(isListOfNumbers, desc='List of deltas to be used cyclically.'), stripeColors=AttrMapValue( isListOfColors, desc='Colors applied cyclically in the right or upper direction.'), fillColor=AttrMapValue(isColorOrNone, desc='Background color for entire rectangle.'), strokeColor=AttrMapValue(isColorOrNone, desc='Color used for lines.'), strokeWidth=AttrMapValue(isNumber, desc='Width used for lines.'), rectStrokeColor=AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'), rectStrokeWidth=AttrMapValue(isNumberOrNone, desc='Width for outer rect stroke.'), ) def __init__(self): self.x = 0 self.y = 0 self.width = 100 self.height = 100 self.orientation = 'vertical' self.useLines = 0 self.useRects = 1 self.delta = 20 self.delta0 = 0 self.deltaSteps = [] self.fillColor = colors.white self.stripeColors = [colors.red, colors.green, colors.blue] self.strokeColor = colors.black self.strokeWidth = 2 def demo(self): D = Drawing(100, 100) g = Grid() D.add(g) return D def makeOuterRect(self): strokeColor = getattr(self, 'rectStrokeColor', self.strokeColor) strokeWidth = getattr(self, 'rectStrokeWidth', self.strokeWidth) if self.fillColor or (strokeColor and strokeWidth): rect = Rect(self.x, self.y, self.width, self.height) rect.fillColor = self.fillColor rect.strokeColor = strokeColor rect.strokeWidth = strokeWidth return rect else: return None def makeLinePosList(self, start, isX=0): "Returns a list of positions where to place lines." w, h = self.width, self.height if isX: length = w else: length = h if self.deltaSteps: r = [start + self.delta0] i = 0 while 1: if r[-1] > start + length: del r[-1] break r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)]) i = i + 1 else: r = frange(start + self.delta0, start + length, self.delta) r.append(start + length) if self.delta0 != 0: r.insert(0, start) #print 'Grid.makeLinePosList() -> %s' % r return r def makeInnerLines(self): # inner grid lines group = Group() w, h = self.width, self.height if self.useLines == 1: if self.orientation == 'vertical': r = self.makeLinePosList(self.x, isX=1) for x in r: line = Line(x, self.y, x, self.y + h) line.strokeColor = self.strokeColor line.strokeWidth = self.strokeWidth group.add(line) elif self.orientation == 'horizontal': r = self.makeLinePosList(self.y, isX=0) for y in r: line = Line(self.x, y, self.x + w, y) line.strokeColor = self.strokeColor line.strokeWidth = self.strokeWidth group.add(line) return group def makeInnerTiles(self): # inner grid lines group = Group() w, h = self.width, self.height # inner grid stripes (solid rectangles) if self.useRects == 1: cols = self.stripeColors if self.orientation == 'vertical': r = self.makeLinePosList(self.x, isX=1) elif self.orientation == 'horizontal': r = self.makeLinePosList(self.y, isX=0) dist = makeDistancesList(r) i = 0 for j in range(len(dist)): if self.orientation == 'vertical': x = r[j] stripe = Rect(x, self.y, dist[j], h) elif self.orientation == 'horizontal': y = r[j] stripe = Rect(self.x, y, w, dist[j]) stripe.fillColor = cols[i % len(cols)] stripe.strokeColor = None group.add(stripe) i = i + 1 return group def draw(self): # general widget bits group = Group() group.add(self.makeOuterRect()) group.add(self.makeInnerTiles()) group.add(self.makeInnerLines(), name='_gridLines') return group
class LegendedHorizontalLineChart(HorizontalLineChart): """A subclass of Legend for drawing legends with lines as the swatches rather than rectangles. Useful for lineCharts and linePlots. Should be similar in all other ways the the standard Legend class. """ _attrMap = AttrMap( BASE=HorizontalLineChart, drawLegend=AttrMapValue(isBoolean, desc='If true draw legend.', advancedUsage=1), legendPositionType=AttrMapValue( OneOf( "null", "top-left", "top-mid", "top-right", "bottom-left", "bottom-mid", "bottom-right" ), desc="The position of LinLegend."), legendAdjustX=AttrMapValue(isNumber, desc='xxx.'), legendAdjustY=AttrMapValue(isNumber, desc='xxx.'), legendCategoryNames=AttrMapValue(isListOfStringsOrNone, desc='List of legend category names.'), titleMain=AttrMapValue(isString, desc='main title text.'), titleMainFontName=AttrMapValue(isString, desc='main title font name.'), titleMainFontSize=AttrMapValue(isNumberInRange(0, 100), desc='main title font size.'), titleMainFontColor=AttrMapValue(isColor, desc='main title font color.'), legendFontSize=AttrMapValue(isNumberInRange(0, 100), desc='legend text font size.'), labels_height=AttrMapValue(isNumberInRange(0, 100), desc='the max height of x-labels.') ) def __init__(self): HorizontalLineChart.__init__(self) self.drawLegend = False self.legendPositionType = "null" self.legendCategoryNames = None self.legendAdjustX = 0 self.legendAdjustY = 0 self.titleMain = "" self.titleMainFontColor = colors.gray self.titleMainFontName = DefaultFontName self.titleMainFontSize = STATE_DEFAULTS['fontSize'] self.legendFontSize = 7 self.labels_height = 0 def set_line_color(self): if self.legendCategoryNames is None: self.legendCategoryNames = [] legend_num = len(self.legendCategoryNames) data_num = len(self.data) for i in range(data_num): line = self.lines[i] line.strokeColor = ALL_COLORS[i] if i >= legend_num: self.legendCategoryNames.append("unknown") legend_num = len(self.legendCategoryNames) temp_category_names = self.legendCategoryNames[:] if legend_num >= 1: self.legendCategoryNames = [] color_name_pairs = [(0, name) for name in temp_category_names] legend_width = ChartsLegend.calc_legend_width( color_name_pairs, 10, 10, DefaultFontName, self.legendFontSize) per_legend_width = int(legend_width / legend_num) legend_num_per_row = int(self.width / per_legend_width) index = 0 row_names = [] for name in temp_category_names: row_names.append(name) index += 1 if index == legend_num_per_row: index = 0 self.legendCategoryNames.append(row_names) row_names = [] if len(row_names) > 0: self.legendCategoryNames.append(row_names) else: self.legendCategoryNames = temp_category_names def _calc_labels_size(self): max_width = 0 index = 0 for label_text in self.categoryAxis.categoryNames: tmp_width = get_string_width(label_text, self.categoryAxis.labels.fontName, self.categoryAxis.labels.fontSize) if tmp_width > max_width: max_width = tmp_width if self.categoryAxis.labels[index].angle % 90 == 0: self.categoryAxis.labels[index].dx = \ int(tmp_width * math.cos(self.categoryAxis.labels[index].angle / 180 * math.pi) / 2) - \ int(self.categoryAxis.labels.fontSize * math.sin(self.categoryAxis.labels[index].angle / 180 * math.pi) / 2) index += 1 self.labels_height = \ int(max_width * math.sin(self.categoryAxis.labels.angle / 180 * math.pi)) + \ int(self.categoryAxis.labels.fontSize * math.cos(self.categoryAxis.labels.angle / 180 * math.pi)) return self.labels_height def _adjust_positon(self): self.x = 30 if self.labels_height > 20: self.y = self.labels_height + 10 else: self.y = 30 self.width -= self.x + 30 self.height -= self.y + self.titleMainFontSize + 20 def draw(self): self._calc_labels_size() self._adjust_positon() self.set_line_color() if self.drawLegend is True: if self.legendPositionType in ["bottom-left", "bottom-mid", "bottom-right"]: row_count = len(self.legendCategoryNames) + 1 self.height -= row_count * self.legendFontSize self.y += row_count * self.legendFontSize g = HorizontalLineChart.draw(self) if self.drawLegend: legend_count = 0 for i in range(len(self.legendCategoryNames)): legend = ChartsLegend() legend.positionType = self.legendPositionType if self.legendPositionType != "null": if self.legendPositionType in ["bottom-left", "bottom-mid", "bottom-right"]: legend.backgroundRect = \ Rect(self.x, self.y + legend.bottom_gap - self.labels_height - 15 - ((i+1) * legend.fontSize), self.width, self.height) else: legend.backgroundRect = Rect(self.x, self.y - (i * legend.fontSize * 1.2), self.width, self.height) legend.adjustX = self.legendAdjustX legend.adjustY = self.legendAdjustY legend.fontSize = self.legendFontSize legend.colorNamePairs = [] for j in range(len(self.legendCategoryNames[i])): legend.colorNamePairs.append((ALL_COLORS[legend_count + j], self.legendCategoryNames[i][j])) legend_count += len(self.legendCategoryNames[i]) g.add(legend) if self.titleMain != "": title = String(0, 0, self.titleMain) title.fontSize = self.titleMainFontSize title.fontName = self.titleMainFontName title.fillColor = self.titleMainFontColor title.textAnchor = 'start' # title.x = self.x - 20 title.x = 0 title.y = self.y + self.height + 20 g.add(title) return g
class SpiderChart(PlotArea): _attrMap = AttrMap(BASE=PlotArea, data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), strands = AttrMapValue(None, desc="collection of strand descriptor objects"), spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"), strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"), spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"), ) def makeSwatchSample(self, rowNo, x, y, width, height): baseStyle = self.strands styleIdx = rowNo % len(baseStyle) style = baseStyle[styleIdx] strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0)) symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None)) ym = y+height/2.0 if fillColor is None and strokeColor is not None and strokeWidth>0: bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor, strokeDashArray=strokeDashArray) elif fillColor is not None: bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, strokeDashArray=strokeDashArray,fillColor=fillColor) else: bg = None if symbol: symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color) if bg: g = Group() g.add(bg) g.add(symbol) return g return symbol or bg def getSeriesName(self,i,default=None): '''return series name i or default''' return getattr(self.strands[i],'name',default) def __init__(self): PlotArea.__init__(self) self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]] self.labels = None # or list of strings self.labels = ['a','b','c','d','e','f'] self.startAngle = 90 self.direction = "clockwise" self.strands = TypedPropertyCollection(StrandProperty) self.spokes = TypedPropertyCollection(SpokeProperty) self.spokeLabels = TypedPropertyCollection(SpokeLabel) self.spokeLabels._text = None self.strandLabels = TypedPropertyCollection(StrandLabel) self.x = 10 self.y = 10 self.width = 180 self.height = 180 def demo(self): d = Drawing(200, 200) d.add(SpiderChart()) return d def normalizeData(self, outer = 0.0): """Turns data into normalized ones where each datum is < 1.0, and 1.0 = maximum radius. Adds 10% at outside edge by default""" data = self.data assert min(map(min,data)) >=0, "Cannot do spider plots of negative numbers!" norm = max(map(max,data)) norm *= (1.0+outer) if norm<1e-9: norm = 1.0 self._norm = norm return [[e/norm for e in row] for row in data] def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=StrandLabel): "Draw a label for a given item in the list." fmt = sty.format value = radius*self._norm if not fmt: text = None elif isinstance(fmt,str): if fmt == 'values': text = sty._text else: text = fmt % value elif callable(fmt): text = fmt(value) else: raise ValueError("Unknown formatter type %s, expected string or function" % fmt) if text: dR = sty.dR if dR: radius += dR/self._radius L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty) if dR<0: L._anti = 1 else: L = None return L def draw(self): # normalize slice data g = self.makeBackground() or Group() xradius = self.width/2.0 yradius = self.height/2.0 self._radius = radius = min(xradius, yradius) cx = self.x + xradius cy = self.y + yradius data = self.normalizeData() self._seriesCount = len(data) n = len(data[0]) #labels if self.labels is None: labels = [''] * n else: labels = self.labels #there's no point in raising errors for less than enough errors if #we silently create all for the extreme case of no labels. i = n-len(labels) if i>0: labels = labels + ['']*i S = [] STRANDS = [] STRANDAREAS = [] syms = [] labs = [] csa = [] angle = self.startAngle*pi/180 direction = self.direction == "clockwise" and -1 or 1 angleBetween = direction*(2 * pi)/float(n) spokes = self.spokes spokeLabels = self.spokeLabels for i in xrange(n): car = cos(angle)*radius sar = sin(angle)*radius csa.append((car,sar,angle)) si = self.spokes[i] if si.visible: spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray) S.append(spoke) sli = spokeLabels[i] text = sli._text if not text: text = labels[i] if text: S.append(_setupLabel(WedgeLabel, text, si.labelRadius, cx, cy, angle, car, sar, sli)) angle += angleBetween # now plot the polygons rowIdx = 0 strands = self.strands strandLabels = self.strandLabels for row in data: # series plot rsty = strands[rowIdx] points = [] car, sar = csa[-1][:2] r = row[-1] points.append(cx+car*r) points.append(cy+sar*r) for i in xrange(n): car, sar, angle = csa[i] r = row[i] points.append(cx+car*r) points.append(cy+sar*r) L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=StrandLabel) if L: labs.append(L) sty = strands[(rowIdx,i)] uSymbol = sty.symbol # put in a marker, if it needs one if uSymbol: s_x = cx+car*r s_y = cy+sar*r s_fillColor = sty.fillColor s_strokeColor = sty.strokeColor s_strokeWidth = sty.strokeWidth s_angle = 0 s_size = sty.symbolSize if type(uSymbol) is type(''): symbol = makeMarker(uSymbol, size = s_size, x = s_x, y = s_y, fillColor = s_fillColor, strokeColor = s_strokeColor, strokeWidth = s_strokeWidth, angle = s_angle, ) else: symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor) for k,v in (('size', s_size), ('fillColor', s_fillColor), ('x', s_x), ('y', s_y), ('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth), ('angle',s_angle),): if getattr(symbol,k,None) is None: try: setattr(symbol,k,v) except: pass syms.append(symbol) # make up the 'strand' if rsty.fillColor: strand = Polygon(points) strand.fillColor = rsty.fillColor strand.strokeColor = None strand.strokeWidth = 0 STRANDAREAS.append(strand) if rsty.strokeColor and rsty.strokeWidth: strand = PolyLine(points) strand.strokeColor = rsty.strokeColor strand.strokeWidth = rsty.strokeWidth strand.strokeDashArray = rsty.strokeDashArray STRANDS.append(strand) rowIdx += 1 map(g.add,STRANDAREAS+STRANDS+syms+S+labs) return g
class Legend(Widget): """A simple legend containing rectangular swatches and strings. The swatches are filled rectangles whenever the respective color object in 'colorNamePairs' is a subclass of Color in reportlab.lib.colors. Otherwise the object passed instead is assumed to have 'x', 'y', 'width' and 'height' attributes. A legend then tries to set them or catches any error. This lets you plug-in any widget you like as a replacement for the default rectangular swatches. Strings can be nicely aligned left or right to the swatches. """ _attrMap = AttrMap( x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"), y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"), deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"), deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"), dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"), autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None"), autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None"), yGap = AttrMapValue(isNumber, desc="Additional gap between rows"), dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"), dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"), columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"), alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"), colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"), fontName = AttrMapValue(isString, desc="Font name of the strings"), fontSize = AttrMapValue(isNumber, desc="Font size of the strings"), fillColor = AttrMapValue(isColorOrNone, desc=""), strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"), strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"), swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ..."), callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))"), boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'), variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)'), dividerLines = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom'), dividerWidth = AttrMapValue(isNumber, desc="dividerLines width"), dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color"), dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.'), dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets'), dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset"), sepSpace = AttrMapValue(isNumber, desc="separator spacing"), colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)"), ) def __init__(self): # Upper-left reference point. self.x = 0 self.y = 0 # Alginment of text with respect to swatches. self.alignment = "left" # x- and y-distances between neighbouring swatches. self.deltax = 75 self.deltay = 20 self.autoXPadding = 5 self.autoYPadding = 2 # Size of swatch rectangle. self.dx = 10 self.dy = 10 # Distance between swatch rectangle and text. self.dxTextSpace = 10 # Max. number of items per column. self.columnMaximum = 3 # Color/name pairs. self.colorNamePairs = [ (colors.red, "red"), (colors.blue, "blue"), (colors.green, "green"), (colors.pink, "pink"), (colors.yellow, "yellow") ] # Font name and size of the labels. self.fontName = STATE_DEFAULTS['fontName'] self.fontSize = STATE_DEFAULTS['fontSize'] self.fillColor = STATE_DEFAULTS['fillColor'] self.strokeColor = STATE_DEFAULTS['strokeColor'] self.strokeWidth = STATE_DEFAULTS['strokeWidth'] self.swatchMarker = None self.boxAnchor = 'nw' self.yGap = 0 self.variColumn = 0 self.dividerLines = 0 self.dividerWidth = 0.5 self.dividerDashArray = None self.dividerColor = colors.black self.dividerOffsX = (0,0) self.dividerOffsY = 0 self.sepSpace = 0 self.colEndCallout = None def _getChartStyleName(self,chart): for a in 'lines', 'bars', 'slices', 'strands': if hasattr(chart,a): return a return None def _getChartStyle(self,chart): return getattr(chart,self._getChartStyleName(chart),None) def _getTexts(self,colorNamePairs): if not isAuto(colorNamePairs): texts = [_getStr(p[1]) for p in colorNamePairs] else: chart = colorNamePairs.chart texts = [str(chart.getSeriesName(i,'series %d' % i)) for i in xrange(chart._seriesCount)] return texts def _calculateMaxWidth(self, colorNamePairs): "Calculate the maximum width of some given strings." M = [] a = M.append for t in self._getTexts(colorNamePairs): M.append(_getWidth(t, self.fontName, self.fontSize,self.sepSpace)) if not M: return 0 if self.variColumn: columnMaximum = self.columnMaximum return [max(M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)] else: return max(M) def _calcHeight(self): dy = self.dy yGap = self.yGap thisy = upperlefty = self.y - dy fontSize = self.fontSize ascent=getFont(self.fontName).face.ascent/1000. if ascent==0: ascent=0.718 # default (from helvetica) ascent *= fontSize leading = fontSize*1.2 deltay = self.deltay if not deltay: deltay = max(dy,leading)+self.autoYPadding columnCount = 0 count = 0 lowy = upperlefty lim = self.columnMaximum - 1 for name in self._getTexts(self.colorNamePairs): y0 = thisy+(dy-ascent)*0.5 y = y0 - _getLineCount(name)*leading leadingMove = 2*y0-y-thisy newy = thisy-max(deltay,leadingMove)-yGap lowy = min(y,newy,lowy) if count==lim: count = 0 thisy = upperlefty columnCount = columnCount + 1 else: thisy = newy count = count+1 return upperlefty - lowy def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): return Rect(x, thisy, dx, dy, fillColor = fillColor, strokeColor = strokeColor, strokeWidth = strokeWidth, ) def draw(self): colorNamePairs = self.colorNamePairs autoCP = isAuto(colorNamePairs) if autoCP: chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None)) swatchMarker = None autoCP = Auto(obj=chart) n = chart._seriesCount chartTexts = self._getTexts(colorNamePairs) else: swatchMarker = getattr(self,'swatchMarker',None) if isAuto(swatchMarker): chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)) swatchMarker = Auto(obj=chart) n = len(colorNamePairs) dx = self.dx dy = self.dy alignment = self.alignment columnMaximum = self.columnMaximum deltax = self.deltax deltay = self.deltay dxTextSpace = self.dxTextSpace fontName = self.fontName fontSize = self.fontSize fillColor = self.fillColor strokeWidth = self.strokeWidth strokeColor = self.strokeColor leading = fontSize*1.2 yGap = self.yGap if not deltay: deltay = max(dy,leading)+self.autoYPadding ba = self.boxAnchor maxWidth = self._calculateMaxWidth(colorNamePairs) nCols = int((n+columnMaximum-1)/columnMaximum) xW = dx+dxTextSpace+self.autoXPadding variColumn = self.variColumn if variColumn: width = reduce(operator.add,maxWidth,0)+xW*(nCols-1) else: deltax = max(maxWidth+xW,deltax) width = maxWidth+(nCols-1)*deltax maxWidth = nCols*[maxWidth] thisx = self.x thisy = self.y - self.dy if ba not in ('ne','n','nw','autoy'): height = self._calcHeight() if ba in ('e','c','w'): thisy += height/2. else: thisy += height if ba not in ('nw','w','sw','autox'): if ba in ('n','c','s'): thisx -= width/2 else: thisx -= width upperlefty = thisy g = Group() def gAdd(t,g=g,fontName=fontName,fontSize=fontSize,fillColor=fillColor): t.fontName = fontName t.fontSize = fontSize t.fillColor = fillColor return g.add(t) ascent=getFont(fontName).face.ascent/1000. if ascent==0: ascent=0.718 # default (from helvetica) ascent *= fontSize # normalize lim = columnMaximum - 1 callout = getattr(self,'callout',None) dividerLines = self.dividerLines if dividerLines: dividerWidth = self.dividerWidth dividerColor = self.dividerColor dividerDashArray = self.dividerDashArray dividerOffsX = self.dividerOffsX dividerOffsY = self.dividerOffsY for i in xrange(n): if autoCP: col = autoCP col.index = i name = chartTexts[i] else: col, name = colorNamePairs[i] if isAuto(swatchMarker): col = swatchMarker col.index = i if isAuto(name): name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i) T = _getLines(name) S = [] j = int(i/columnMaximum) # thisy+dy/2 = y+leading/2 y = y0 = thisy+(dy-ascent)*0.5 if callout: callout(self,g,thisx,y,(col,name)) if alignment == "left": if isSeqType(name): for t in T[0]: S.append(String(thisx,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "start")) y -= leading yd = y y = y0 for t in T[1]: S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "end")) y -= leading y = min(yd,y) else: for t in T: # align text to left S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "end")) y -= leading x = thisx+maxWidth[j]+dxTextSpace elif alignment == "right": if isSeqType(name): y0 = y for t in T[0]: S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "start")) y -= leading yd = y y = y0 for t in T[1]: S.append(String(thisx+dx+dxTextSpace+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "end")) y -= leading y = min(yd,y) else: for t in T: # align text to right S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, textAnchor = "start")) y -= leading x = thisx else: raise ValueError, "bad alignment" leadingMove = 2*y0-y-thisy if dividerLines: xd = thisx+dx+dxTextSpace+maxWidth[j]+dividerOffsX[1] yd = thisy+dy*0.5+dividerOffsY if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum): g.add(Line(thisx+dividerOffsX[0],yd,xd,yd, strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)): yd -= max(deltay,leadingMove)+yGap g.add(Line(thisx+dividerOffsX[0],yd,xd,yd, strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) # Make a 'normal' color swatch... if isAuto(col): chart = getattr(col,'chart',getattr(col,'obj',None)) g.add(chart.makeSwatchSample(getattr(col,'index',i),x,thisy,dx,dy)) elif isinstance(col, colors.Color): if isSymbol(swatchMarker): g.add(uSymbol2Symbol(swatchMarker,x+dx/2.,thisy+dy/2.,col)) else: g.add(self._defaultSwatch(x,thisy,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)) else: try: c = copy.deepcopy(col) c.x = x c.y = thisy c.width = dx c.height = dy g.add(c) except: pass map(gAdd,S) if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)): if alignment == "left": xt = thisx else: xt = thisx+dx+dxTextSpace yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap) self.colEndCallout(self, g, thisx, xt, yd, maxWidth[j], maxWidth[j]+dx+dxTextSpace) if i%columnMaximum==lim: if variColumn: thisx += maxWidth[j]+xW else: thisx = thisx+deltax thisy = upperlefty else: thisy = thisy-max(deltay,leadingMove)-yGap return g def demo(self): "Make sample legend." d = Drawing(200, 100) legend = Legend() legend.alignment = 'left' legend.x = 0 legend.y = 100 legend.dxTextSpace = 5 items = 'red green blue yellow pink black white'.split() items = map(lambda i:(getattr(colors, i), i), items) legend.colorNamePairs = items d.add(legend, 'legend') return d
class ChartsLegend(LineLegend): """A subclass of Legend for drawing legends with lines as the swatches rather than rectangles. Useful for lineCharts and linePlots. Should be similar in all other ways the the standard Legend class. """ _attrMap = AttrMap( BASE=LineLegend, positionType=AttrMapValue(OneOf("null", "top-left", "top-mid", "top-right", "bottom-left", "bottom-mid", "bottom-right", "right"), desc="The position of LinLegend."), backgroundRect=AttrMapValue(None, desc="The position of LinLegend."), adjustX=AttrMapValue(isNumber, desc='xxx.'), adjustY=AttrMapValue(isNumber, desc='xxx.'), bottom_gap=AttrMapValue(isNumber, desc='xxx.')) def __init__(self): LineLegend.__init__(self) self.positionType = "null" self.backgroundRect = None self.adjustX = 0 self.adjustY = 0 self.fontName = DefaultFontName self.deltax = 10 self.deltay = 0 self.boxAnchor = 'w' self.columnMaximum = 1 self.yGap = 0 self.fontSize = 7 self.alignment = 'right' self.dxTextSpace = 5 self.bottom_gap = 40 @staticmethod def calc_legend_width(color_name_pairs, dx, deltax, font_name, font_size, sub_cols=None): pairs_num = len(color_name_pairs) max_text_width = 0 x_width = 0 for x in color_name_pairs: if type(x[1]) is tuple: for str_i in x[1]: tmp_width = stringWidth(str(str_i), font_name, font_size) if sub_cols is not None and tmp_width < sub_cols[ 0].minWidth: tmp_width = sub_cols[0].minWidth x_width += tmp_width else: str_x = x[1] x_width = stringWidth(str_x, font_name, font_size) if x_width > max_text_width: max_text_width = x_width total_text_width = (pairs_num - 1) * max_text_width + x_width legend_width = total_text_width + (dx * pairs_num) + (deltax * pairs_num) return legend_width def draw(self): legend_width = self.calc_legend_width(self.colorNamePairs, self.dx, self.deltax, self.fontName, self.fontSize, self.subCols) if self.positionType != "null" and self.backgroundRect is not None: if self.positionType == "top-left": self.x = self.backgroundRect.x self.y = self.backgroundRect.y + self.backgroundRect.height elif self.positionType == "top-mid": self.x = self.backgroundRect.x + int( self.backgroundRect.width / 2) - int(legend_width / 2) self.y = self.backgroundRect.y + self.backgroundRect.height elif self.positionType == "top-right": self.x = self.backgroundRect.x + self.backgroundRect.width - legend_width self.y = self.backgroundRect.y + self.backgroundRect.height elif self.positionType == "bottom-left": self.x = self.backgroundRect.x self.y = self.backgroundRect.y - self.bottom_gap elif self.positionType == "bottom-mid": self.x = self.backgroundRect.x + int( self.backgroundRect.width / 2) - int(legend_width / 2) self.y = self.backgroundRect.y - self.bottom_gap elif self.positionType == "bottom-right": self.x = self.backgroundRect.x + self.backgroundRect.width - legend_width self.y = self.backgroundRect.y - self.bottom_gap elif self.positionType == "right": self.x = self.backgroundRect.x + self.backgroundRect.width + 10 self.y = self.backgroundRect.y + self.backgroundRect.height self.x += self.adjustX self.y += self.adjustY return LineLegend.draw(self)
class Label(Widget): """A text label to attach to something else, such as a chart axis. This allows you to specify an offset, angle and many anchor properties relative to the label's origin. It allows, for example, angled multiline axis labels. """ # fairly straight port of Robin Becker's textbox.py to new widgets # framework. _attrMap = AttrMap( x=AttrMapValue(isNumber, desc=''), y=AttrMapValue(isNumber, desc=''), dx=AttrMapValue(isNumber, desc='delta x - offset'), dy=AttrMapValue(isNumber, desc='delta y - offset'), angle=AttrMapValue( isNumber, desc= 'angle of label: default (0), 90 is vertical, 180 is upside down, etc' ), boxAnchor=AttrMapValue(isBoxAnchor, desc='anchoring point of the label'), boxStrokeColor=AttrMapValue(isColorOrNone, desc='border color of the box'), boxStrokeWidth=AttrMapValue(isNumber, desc='border width'), boxFillColor=AttrMapValue(isColorOrNone, desc='the filling color of the box'), boxTarget=AttrMapValue(OneOf('normal', 'anti', 'lo', 'hi'), desc="one of ('normal','anti','lo','hi')"), fillColor=AttrMapValue(isColorOrNone, desc='label text color'), strokeColor=AttrMapValue(isColorOrNone, desc='label text border color'), strokeWidth=AttrMapValue(isNumber, desc='label text border width'), text=AttrMapValue(isString, desc='the actual text to display'), fontName=AttrMapValue(isString, desc='the name of the font used'), fontSize=AttrMapValue(isNumber, desc='the size of the font'), leading=AttrMapValue(isNumberOrNone, desc=''), width=AttrMapValue(isNumberOrNone, desc='the width of the label'), maxWidth=AttrMapValue(isNumberOrNone, desc='maximum width the label can grow to'), height=AttrMapValue(isNumberOrNone, desc='the height of the text'), textAnchor=AttrMapValue( isTextAnchor, desc='the anchoring point of the text inside the label'), visible=AttrMapValue(isBoolean, desc="True if the label is to be drawn"), topPadding=AttrMapValue(isNumber, desc='padding at top of box'), leftPadding=AttrMapValue(isNumber, desc='padding at left of box'), rightPadding=AttrMapValue(isNumber, desc='padding at right of box'), bottomPadding=AttrMapValue(isNumber, desc='padding at bottom of box'), useAscentDescent=AttrMapValue( isBoolean, desc= "If True then the font's Ascent & Descent will be used to compute default heights and baseline." ), customDrawChanger=AttrMapValue( isNoneOrCallable, desc= "An instance of CustomDrawChanger to modify the behavior at draw time", _advancedUsage=1), ) def __init__(self, **kw): self._setKeywords(**kw) self._setKeywords( _text='Multi-Line\nString', boxAnchor='c', angle=0, x=0, y=0, dx=0, dy=0, topPadding=0, leftPadding=0, rightPadding=0, bottomPadding=0, boxStrokeWidth=0.5, boxStrokeColor=None, boxTarget='normal', strokeColor=None, boxFillColor=None, leading=None, width=None, maxWidth=None, height=None, fillColor=STATE_DEFAULTS['fillColor'], fontName=STATE_DEFAULTS['fontName'], fontSize=STATE_DEFAULTS['fontSize'], strokeWidth=0.1, textAnchor='start', visible=1, useAscentDescent=False, ) def setText(self, text): """Set the text property. May contain embedded newline characters. Called by the containing chart or axis.""" self._text = text def setOrigin(self, x, y): """Set the origin. This would be the tick mark or bar top relative to which it is defined. Called by the containing chart or axis.""" self.x = x self.y = y def demo(self): """This shows a label positioned with its top right corner at the top centre of the drawing, and rotated 45 degrees.""" d = Drawing(200, 100) # mark the origin of the label d.add(Circle(100, 90, 5, fillColor=colors.green)) lab = Label() lab.setOrigin(100, 90) lab.boxAnchor = 'ne' lab.angle = 45 lab.dx = 0 lab.dy = -20 lab.boxStrokeColor = colors.green lab.setText('Another\nMulti-Line\nString') d.add(lab) return d def _getBoxAnchor(self): '''hook for allowing special box anchor effects''' ba = self.boxAnchor if ba in ('autox', 'autoy'): angle = self.angle na = (int((angle % 360) / 45.) * 45) % 360 if not (na % 90): # we have a right angle case da = (angle - na) % 360 if abs(da) > 5: na = na + (da > 0 and 45 or -45) ba = _A2BA[ba[-1]][na] return ba def computeSize(self): # the thing will draw in its own coordinate system self._lineWidths = [] topPadding = self.topPadding leftPadding = self.leftPadding rightPadding = self.rightPadding bottomPadding = self.bottomPadding self._lines = simpleSplit(self._text, self.fontName, self.fontSize, self.maxWidth) if not self.width: self._width = leftPadding + rightPadding if self._lines: self._lineWidths = [ stringWidth(line, self.fontName, self.fontSize) for line in self._lines ] self._width += max(self._lineWidths) else: self._width = self.width if self.useAscentDescent: self._ascent, self._descent = getAscentDescent( self.fontName, self.fontSize) self._baselineRatio = self._ascent / (self._ascent - self._descent) else: self._baselineRatio = 1 / 1.2 if self.leading: self._leading = self.leading elif self.useAscentDescent: self._leading = self._ascent - self._descent else: self._leading = self.fontSize * 1.2 self._height = self.height or (self._leading * len(self._lines) + topPadding + bottomPadding) self._ewidth = (self._width - leftPadding - rightPadding) self._eheight = (self._height - topPadding - bottomPadding) boxAnchor = self._getBoxAnchor() if boxAnchor in ['n', 'ne', 'nw']: self._top = -topPadding elif boxAnchor in ['s', 'sw', 'se']: self._top = self._height - topPadding else: self._top = 0.5 * self._eheight self._bottom = self._top - self._eheight if boxAnchor in ['ne', 'e', 'se']: self._left = leftPadding - self._width elif boxAnchor in ['nw', 'w', 'sw']: self._left = leftPadding else: self._left = -self._ewidth * 0.5 self._right = self._left + self._ewidth def _getTextAnchor(self): '''This can be overridden to allow special effects''' ta = self.textAnchor if ta == 'boxauto': ta = _BA2TA[self._getBoxAnchor()] return ta def _rawDraw(self): _text = self._text self._text = _text or '' self.computeSize() self._text = _text g = Group() g.translate(self.x + self.dx, self.y + self.dy) g.rotate(self.angle) y = self._top - self._leading * self._baselineRatio textAnchor = self._getTextAnchor() if textAnchor == 'start': x = self._left elif textAnchor == 'middle': x = self._left + self._ewidth * 0.5 else: x = self._right # paint box behind text just in case they # fill it if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth): g.add( Rect(self._left - self.leftPadding, self._bottom - self.bottomPadding, self._width, self._height, strokeColor=self.boxStrokeColor, strokeWidth=self.boxStrokeWidth, fillColor=self.boxFillColor)) fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, self._leading svgAttrs = getattr(self, '_svgAttrs', {}) if strokeColor: for line in self._lines: s = _text2Path(line, x, y, fontName, fontSize, textAnchor) s.fillColor = fillColor s.strokeColor = strokeColor s.strokeWidth = strokeWidth g.add(s) y -= leading else: for line in self._lines: s = String(x, y, line, _svgAttrs=svgAttrs) s.textAnchor = textAnchor s.fontName = fontName s.fontSize = fontSize s.fillColor = fillColor g.add(s) y -= leading return g def draw(self): customDrawChanger = getattr(self, 'customDrawChanger', None) if customDrawChanger: customDrawChanger(True, self) try: return self._rawDraw() finally: customDrawChanger(False, self) else: return self._rawDraw()
class ShadedRect(Widget): """This makes a rectangle with shaded colors between two colors. Colors are interpolated linearly between 'fillColorStart' and 'fillColorEnd', both of which appear at the margins. If 'numShades' is set to one, though, only 'fillColorStart' is used. """ _attrMap = AttrMap( x=AttrMapValue(isNumber, desc="The grid's lower-left x position."), y=AttrMapValue(isNumber, desc="The grid's lower-left y position."), width=AttrMapValue(isNumber, desc="The grid's width."), height=AttrMapValue(isNumber, desc="The grid's height."), orientation=AttrMapValue( OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'), numShades=AttrMapValue(isNumber, desc='The number of interpolating colors.'), fillColorStart=AttrMapValue(isColorOrNone, desc='Start value of the color shade.'), fillColorEnd=AttrMapValue(isColorOrNone, desc='End value of the color shade.'), strokeColor=AttrMapValue(isColorOrNone, desc='Color used for border line.'), strokeWidth=AttrMapValue(isNumber, desc='Width used for lines.'), cylinderMode=AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), ) def __init__(self, **kw): self.x = 0 self.y = 0 self.width = 100 self.height = 100 self.orientation = 'vertical' self.numShades = 20 self.fillColorStart = colors.pink self.fillColorEnd = colors.black self.strokeColor = colors.black self.strokeWidth = 2 self.cylinderMode = 0 self.setProperties(kw) def demo(self): D = Drawing(100, 100) g = ShadedRect() D.add(g) return D def _flipRectCorners(self): "Flip rectangle's corners if width or height is negative." x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd if width < 0 and height > 0: x = x + width width = -width if self.orientation == 'vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart elif height < 0 and width > 0: y = y + height height = -height if self.orientation == 'horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart elif height < 0 and height < 0: x = x + width width = -width y = y + height height = -height return x, y, width, height, fillColorStart, fillColorEnd def draw(self): # general widget bits group = Group() x, y, w, h, c0, c1 = self._flipRectCorners() numShades = self.numShades if self.cylinderMode: if not numShades % 2: numShades = numShades + 1 halfNumShades = (numShades - 1) / 2 + 1 num = float(numShades) # must make it float! vertical = self.orientation == 'vertical' if vertical: if numShades == 1: V = [x] else: V = frange(x, x + w, w / num) else: if numShades == 1: V = [y] else: V = frange(y, y + h, h / num) for v in V: stripe = vertical and Rect(v, y, w / num, h) or Rect( x, v, w, h / num) if self.cylinderMode: if V.index(v) >= halfNumShades: col = colors.linearlyInterpolatedColor( c1, c0, V[halfNumShades], V[-1], v) else: col = colors.linearlyInterpolatedColor( c0, c1, V[0], V[halfNumShades], v) else: col = colors.linearlyInterpolatedColor(c0, c1, V[0], V[-1], v) stripe.fillColor = col stripe.strokeColor = col stripe.strokeWidth = 1 group.add(stripe) if self.strokeColor and self.strokeWidth >= 0: rect = Rect(x, y, w, h) rect.strokeColor = self.strokeColor rect.strokeWidth = self.strokeWidth rect.fillColor = None group.add(rect) return group
class Pie(AbstractPieChart): _attrMap = AttrMap(BASE=AbstractPieChart, data = AttrMapValue(isListOfNumbers, desc='list of numbers defining wedge sizes; need not sum to 1'), labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), slices = AttrMapValue(None, desc="collection of wedge descriptor objects"), simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"), other_threshold = AttrMapValue(isNumber, desc='A value for doing threshholding, not used yet.'), checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix standard label overlaps(default off)"), pointerLabelMode = AttrMapValue(OneOf(None,'LeftRight','LeftAndRight'), desc=""), sameRadii = AttrMapValue(isBoolean, desc="If true make x/y radii the same(default off)"), orderMode = AttrMapValue(OneOf('fixed','alternate')), xradius = AttrMapValue(isNumberOrNone, desc="X direction Radius"), yradius = AttrMapValue(isNumberOrNone, desc="Y direction Radius"), ) other_threshold=None def __init__(self,**kwd): PlotArea.__init__(self) self.x = 0 self.y = 0 self.width = 100 self.height = 100 self.data = [1,2.3,1.7,4.2] self.labels = None # or list of strings self.startAngle = 90 self.direction = "clockwise" self.simpleLabels = 1 self.checkLabelOverlap = 0 self.pointerLabelMode = None self.sameRadii = False self.orderMode = 'fixed' self.xradius = self.yradius = None self.slices = TypedPropertyCollection(WedgeProperties) self.slices[0].fillColor = colors.darkcyan self.slices[1].fillColor = colors.blueviolet self.slices[2].fillColor = colors.blue self.slices[3].fillColor = colors.cyan self.slices[4].fillColor = colors.pink self.slices[5].fillColor = colors.magenta self.slices[6].fillColor = colors.yellow def demo(self): d = Drawing(200, 100) pc = Pie() pc.x = 50 pc.y = 10 pc.width = 100 pc.height = 80 pc.data = [10,20,30,40,50,60] pc.labels = ['a','b','c','d','e','f'] pc.slices.strokeWidth=0.5 pc.slices[3].popout = 10 pc.slices[3].strokeWidth = 2 pc.slices[3].strokeDashArray = [2,2] pc.slices[3].labelRadius = 1.75 pc.slices[3].fontColor = colors.red pc.slices[0].fillColor = colors.darkcyan pc.slices[1].fillColor = colors.blueviolet pc.slices[2].fillColor = colors.blue pc.slices[3].fillColor = colors.cyan pc.slices[4].fillColor = colors.aquamarine pc.slices[5].fillColor = colors.cadetblue pc.slices[6].fillColor = colors.lightcoral d.add(pc) return d def makePointerLabels(self,angles,plMode): class PL: def __init__(self,centerx,centery,xradius,yradius,data,lu=0,ru=0): self.centerx = centerx self.centery = centery self.xradius = xradius self.yradius = yradius self.data = data self.lu = lu self.ru = ru labelX = self.width-2 labelY = self.height n = nr = nl = maxW = sumH = 0 styleCount = len(self.slices) L=[] L_add = L.append refArcs = _makeSideArcDefs(self.startAngle,self.direction) for i, A in angles: if A[1] is None: continue sn = self.getSeriesName(i,'') if not sn: continue style = self.slices[i%styleCount] if not style.label_visible or not style.visible: continue n += 1 _addWedgeLabel(self,sn,L_add,180,labelX,labelY,style,labelClass=WedgeLabel) l = L[-1] b = l.getBounds() w = b[2]-b[0] h = b[3]-b[1] ri = [(a[0],intervalIntersection(A,(a[1],a[2]))) for a in refArcs] li = _findLargestArc(ri,0) ri = _findLargestArc(ri,1) if li and ri: if plMode=='LeftAndRight': if li[1]-li[0]<ri[1]-ri[0]: li = None else: ri = None else: if li[1]-li[0]<0.02*(ri[1]-ri[0]): li = None elif (li[1]-li[0])*0.02>ri[1]-ri[0]: ri = None if ri: nr += 1 if li: nl += 1 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) maxW = max(w,maxW) sumH += h+2 if not n: #we have no labels xradius = self.width*0.5 yradius = self.height*0.5 centerx = self.x+xradius centery = self.y+yradius if self.xradius: xradius = self.xradius if self.yradius: yradius = self.yradius if self.sameRadii: xradius=yradius=min(xradius,yradius) return PL(centerx,centery,xradius,yradius,[]) aonR = nr==n if sumH<self.height and (aonR or nl==n): side=int(aonR) else: side=None G,lu,ru,mel = _fixPointerLabels(len(angles),L,self.x,self.y,self.width,self.height,side=side) if plMode=='LeftAndRight': lu = ru = max(lu,ru) x0 = self.x+lu x1 = self.x+self.width-ru xradius = (x1-x0)*0.5 yradius = self.height*0.5-mel centerx = x0+xradius centery = self.y+yradius+mel if self.xradius: xradius = self.xradius if self.yradius: yradius = self.yradius if self.sameRadii: xradius=yradius=min(xradius,yradius) return PL(centerx,centery,xradius,yradius,G,lu,ru) def normalizeData(self): data = map(abs,self.data) s = self._sum = float(sum(data)) if s>1e-8: f = 360./s return [f*x for x in data] else: return [0]*len(data) def makeAngles(self): startAngle = self.startAngle % 360 whichWay = self.direction == "clockwise" and -1 or 1 D = [a for a in enumerate(self.normalizeData())] if self.orderMode=='alternate': W = [a for a in D if abs(a[1])>=1e-5] W.sort(_arcCF) T = [[],[]] i = 0 while W: if i<2: a = W.pop(0) else: a = W.pop(-1) T[i%2].append(a) i += 1 i %= 4 T[1].reverse() D = T[0]+T[1] + [a for a in D if abs(a[1])<1e-5] A = [] a = A.append for i, angle in D: endAngle = (startAngle + (angle * whichWay)) if abs(angle)>=1e-5: if startAngle >= endAngle: aa = endAngle,startAngle else: aa = startAngle,endAngle else: aa = startAngle, None startAngle = endAngle a((i,aa)) return A def makeWedges(self): angles = self.makeAngles() n = len(angles) labels = _fixLabels(self.labels,n) self._seriesCount = n styleCount = len(self.slices) plMode = self.pointerLabelMode if plMode: checkLabelOverlap = False PL=self.makePointerLabels(angles,plMode) xradius = PL.xradius yradius = PL.yradius centerx = PL.centerx centery = PL.centery PL_data = PL.data gSN = lambda i: '' else: xradius = self.width*0.5 yradius = self.height*0.5 centerx = self.x + xradius centery = self.y + yradius if self.xradius: xradius = self.xradius if self.yradius: yradius = self.yradius if self.sameRadii: xradius=yradius=min(xradius,yradius) checkLabelOverlap = self.checkLabelOverlap gSN = lambda i: self.getSeriesName(i,'') g = Group() g_add = g.add if checkLabelOverlap: L = [] L_add = L.append else: L_add = g_add for i,(a1,a2) in angles: if a2 is None: continue #if we didn't use %stylecount here we'd end up with the later wedges #all having the default style wedgeStyle = self.slices[i%styleCount] if not wedgeStyle.visible: continue # is it a popout? cx, cy = centerx, centery text = gSN(i) popout = wedgeStyle.popout if text or popout: averageAngle = (a1+a2)/2.0 aveAngleRadians = averageAngle/_180_pi cosAA = cos(aveAngleRadians) sinAA = sin(aveAngleRadians) if popout: # pop out the wedge cx = centerx + popout*cosAA cy = centery + popout*sinAA if n > 1: theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius) elif n==1: theWedge = Ellipse(cx, cy, xradius, yradius) theWedge.fillColor = wedgeStyle.fillColor theWedge.strokeColor = wedgeStyle.strokeColor theWedge.strokeWidth = wedgeStyle.strokeWidth theWedge.strokeDashArray = wedgeStyle.strokeDashArray g_add(theWedge) if wedgeStyle.label_visible: if text: labelRadius = wedgeStyle.labelRadius rx = xradius*labelRadius ry = yradius*labelRadius labelX = cx + rx*cosAA labelY = cy + ry*sinAA _addWedgeLabel(self,text,L_add,averageAngle,labelX,labelY,wedgeStyle) if checkLabelOverlap: l = L[-1] l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle, 'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy, 'bounds': l.getBounds(), } elif plMode and PL_data: l = PL_data[i] if l: data = l._origdata sinM = data['smid'] cosM = data['cmid'] lX = cx + xradius*cosM lY = cy + yradius*sinM lpel = wedgeStyle.label_pointer_elbowLength lXi = lX + lpel*cosM lYi = lY + lpel*sinM L_add(PolyLine((lX,lY,lXi,lYi,l.x,l.y), strokeWidth=wedgeStyle.label_pointer_strokeWidth, strokeColor=wedgeStyle.label_pointer_strokeColor)) L_add(l) if checkLabelOverlap and L: fixLabelOverlaps(L) map(g_add,L) return g def draw(self): G = self.makeBackground() w = self.makeWedges() if G: return Group(G,w) return w
class ReportLabBarChart(BarChart): _flipXY = 0 _attrMap = AttrMap( BASE=BarChart, drawLegend=AttrMapValue(isBoolean, desc='If true draw legend.', advancedUsage=1), legendPositionType=AttrMapValue(OneOf("null", "top-left", "top-mid", "top-right", "bottom-left", "bottom-mid", "bottom-right"), desc="The position of LinLegend."), legendAdjustX=AttrMapValue(isNumber, desc='xxx.'), legendAdjustY=AttrMapValue(isNumber, desc='xxx.'), legendCategoryNames=AttrMapValue( isListOfStringsOrNone, desc='List of legend category names.'), titleMain=AttrMapValue(isString, desc='main title text.'), titleMainFontName=AttrMapValue(isString, desc='main title font name.'), titleMainFontSize=AttrMapValue(isNumberInRange(0, 100), desc='main title font size.'), titleMainFontColor=AttrMapValue(isColor, desc='main title font color.'), legendFontSize=AttrMapValue(isNumberInRange(0, 100), desc='legend text font size.'), x_labels_height=AttrMapValue(isNumberInRange(0, 100), desc='the max height in x-labels.'), y_labels_height=AttrMapValue(isNumberInRange(0, 50), desc='the max height in y-labels.')) def __init__(self, x, y, width, height, cat_names, data, step_count=4, style="parallel", label_format=None, label_sum=False, legend_names=None, legend_position="top-right", legend_adjust_x=0, legend_adjust_y=0, main_title="", main_title_font_name=None, main_title_font_size=None, main_title_font_color=None, x_desc=None, y_desc=None, cat_label_angle=30, cat_label_all=False): BarChart.__init__(self) if self._flipXY: self.categoryAxis = YCategoryAxisWithDesc(desc=y_desc) self.valueAxis = XValueAxisWithDesc(desc=x_desc) else: self.categoryAxis = XCategoryAxisWithDesc(desc=x_desc) self.valueAxis = YValueAxisWithDesc(desc=y_desc) if style not in ["stacked", "parallel"]: style = "parallel" self.categoryAxis.style = style self.valueAxis.visibleGrid = 1 self.valueAxis.gridStrokeColor = colors.Color(0.5, 0.5, 0.5, 0.5) self.valueAxis.gridStrokeWidth = 1 self.x = x self.y = y self.height = height self.width = width self.data = data self.strokeColor = colors.black self.categoryAxis.labels.boxAnchor = 'ne' # self.categoryAxis.labels.dx = 0 # self.categoryAxis.labels.dy = 0 self.categoryAxis.labels.angle = cat_label_angle # self.categoryAxis.labels.boxFillColor = colors.Color(1, 0, 0, 1) if cat_label_all is False: cat_names_num = len(cat_names) show_cat_num = 4 if cat_names_num > show_cat_num: gap_num = int(cat_names_num / show_cat_num) for i in range(cat_names_num): if i % gap_num != 0: cat_names[i] = "" self.categoryAxis.categoryNames = cat_names self._lable_sum = [] if label_format is not None: self.barLabelFormat = label_format if len(data) > 1 and style == "stacked": if label_sum: self._cal_col_sum() self.barLabels.boxTarget = "hi" self.barLabels.nudge = 15 else: self.barLabels.boxTarget = "mid" else: self.barLabels.boxTarget = "hi" self.barLabels.nudge = 15 min_value, max_value, step = self.get_limit_value(step_count) self.valueAxis.valueMin = min_value self.valueAxis.valueMax = max_value self.valueAxis.valueStep = step self.drawLegend = False self.legendCategoryNames = None if legend_names is not None and isListOfStrings(legend_names) is True: self.drawLegend = True self.legendCategoryNames = legend_names self.legendPositionType = legend_position self.legendAdjustX = legend_adjust_x self.legendAdjustY = legend_adjust_y self.legendFontSize = 7 self.titleMain = main_title self.titleMainFontName = DefaultFontName self.titleMainFontSize = STATE_DEFAULTS['fontSize'] self.titleMainFontColor = colors.black if main_title_font_name is not None: self.titleMainFontName = main_title_font_name if main_title_font_size is not None: self.titleMainFontSize = main_title_font_size if main_title_font_color is not None: self.titleMainFontColor = main_title_font_color self.x_labels_height = 0 self.y_labels_height = 0 def _cal_col_sum(self): for i in range(len(self.data[0])): self._lable_sum.append(0) for d in self.data: idx = 0 for i in d: self._lable_sum[idx] += i idx += 1 def get_limit_value(self, step_count): min_value = 0xffffffff max_value = 0 - min_value _data = [] if self.categoryAxis.style == "stacked": flag = True for d in self.data: idx = 0 for i in d: if flag: _data.append(i) else: _data[idx] += i idx += 1 flag = False for d in _data: if d > max_value: max_value = d for d in self.data: for i in d: if i < min_value: min_value = i else: _data = self.data[:] for d in _data: for i in d: if i > max_value: max_value = i if i < min_value: min_value = i max_value += int(max_value / 10) max_value = int(max_value / 5) * 5 min_value -= int(min_value / 10) min_value = int(min_value / 5) * 5 step = int((max_value - min_value) / step_count) step = int(step / 5 + 1) * 5 max_value = min_value + (step * step_count) return min_value, max_value, step def set_bar_color(self): if self.legendCategoryNames is None: self.legendCategoryNames = [] legend_num = len(self.legendCategoryNames) data_num = len(self.data) for i in range(data_num): bar = self.bars[i] bar.strokeColor = ALL_COLORS[i] bar.fillColor = ALL_COLORS[i] if i >= legend_num: self.legendCategoryNames.append("unknown") legend_num = len(self.legendCategoryNames) temp_category_names = self.legendCategoryNames[:] if legend_num >= 1: self.legendCategoryNames = [] color_name_pairs = [(0, name) for name in temp_category_names] legend_width = ChartsLegend.calc_legend_width( color_name_pairs, 10, 10, DefaultFontName, self.legendFontSize) per_legend_width = int(legend_width / legend_num) legend_num_per_row = int(self.width / per_legend_width) index = 0 row_names = [] for name in temp_category_names: row_names.append(name) index += 1 if index == legend_num_per_row: index = 0 self.legendCategoryNames.append(row_names) row_names = [] if len(row_names) > 0: self.legendCategoryNames.append(row_names) else: self.legendCategoryNames = temp_category_names def _draw_legend(self, g): legend_count = 0 for i in range(len(self.legendCategoryNames)): legend = ChartsLegend() legend.positionType = self.legendPositionType if self.legendPositionType != "null": if self.legendPositionType in [ "bottom-left", "bottom-mid", "bottom-right" ]: legend.backgroundRect = \ Rect(self.x, self.y + legend.bottom_gap - self.x_labels_height - 15 - ((i + 1) * legend.fontSize), self.width, self.height) else: legend.backgroundRect = Rect( self.x, self.y + (i * legend.fontSize * 1.2), self.width, self.height) legend.adjustX = self.legendAdjustX legend.adjustY = self.legendAdjustY legend.fontSize = self.legendFontSize legend.colorNamePairs = [] for j in range(len(self.legendCategoryNames[i])): legend.colorNamePairs.append((ALL_COLORS[legend_count + j], self.legendCategoryNames[i][j])) legend_count += len(self.legendCategoryNames[i]) g.add(legend) def _get_label_sum_text(self, row_no, col_no): """ return formatted label text :param row_no: :param col_no: :return: """ len_row = len(self.data) if row_no != len_row - 1: return None text = self._lable_sum[col_no] label_fmt = self.barLabelFormat if isinstance(label_fmt, (list, tuple)): label_fmt = label_fmt[row_no] if isinstance(label_fmt, (list, tuple)): label_fmt = label_fmt[col_no] if label_fmt is None: label_text = None elif label_fmt == 'values': label_text = text elif isStr(label_fmt): label_text = label_fmt % text elif hasattr(label_fmt, '__call__'): label_text = label_fmt(text) else: msg = "Unknown formatter type %s, expected string or function" % label_fmt raise Exception(msg) return label_text def _addBarLabel(self, g, row_no, col_no, x, y, width, height): if self._lable_sum: text = self._get_label_sum_text(row_no, col_no) else: text = self._getLabelText(row_no, col_no) if text: self._addLabel(text, self.barLabels[(row_no, col_no)], g, row_no, col_no, x, y, width, height) def draw(self): self.set_bar_color() if self.drawLegend is True: if self.legendPositionType in [ "bottom-left", "bottom-mid", "bottom-right" ]: row_count = len(self.legendCategoryNames) + 1 self.height -= row_count * self.legendFontSize self.y += row_count * self.legendFontSize g = BarChart.draw(self) if self.drawLegend is True: self._draw_legend(g) if self.titleMain != "": title = String(0, 0, self.titleMain) title.fontSize = self.titleMainFontSize title.fontName = self.titleMainFontName title.fillColor = self.titleMainFontColor title.textAnchor = 'start' # title.x = self.x - 20 # title.y = self.y + self.height + 20 title.x = 0 title.y = self.y + self.height + 20 g.add(title) return g