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), swdx=AttrMapValue(isNumber, desc="x position adjustment for the swatch"), swdy=AttrMapValue(isNumber, desc="y position adjustment for the swatch"), ) 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 self.swdx = 0 self.swdy = 0 # 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.dx = sc.dy = 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 range(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 range(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 range(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 isSeq(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 scdx = sc.dx scdy = sc.dy 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 + scdx, y + scdy, 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... swatchX = x + getattr(self, 'swdx', 0) swatchY = thisy + getattr(self, 'swdy', 0) if isAuto(col): chart = getattr(col, 'chart', getattr(col, 'obj', None)) c = chart.makeSwatchSample(getattr(col, 'index', i), swatchX, swatchY, dx, dy) elif isinstance(col, colors.Color): if isSymbol(swatchMarker): c = uSymbol2Symbol(swatchMarker, swatchX + dx / 2., swatchY + dy / 2., col) else: c = self._defaultSwatch(swatchX, swatchY, dx, dy, fillColor=col, strokeWidth=strokeWidth, strokeColor=strokeColor) elif col is not None: try: c = copy.deepcopy(col) c.x = swatchX c.y = swatchY 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 = [(getattr(colors, i), i) for i in items] legend.colorNamePairs = items d.add(legend, 'legend') return d
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 = int((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 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 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 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
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 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 _objStr(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(list(map( min, data))) >= 0, "Cannot do spider plots of negative numbers!" norm = max(list(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 hasattr(fmt, '__call__'): 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 range(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 range(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 for s in (STRANDAREAS + STRANDS + syms + S + labs): g.add(s) 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( 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 list( map(lambda x, f=360. / sum: f * x, data)) or len(data) * [0] def makeSectors(self): # normalize slice data if isinstance(self.data, (list, tuple)) and isinstance(self.data[0], (list, tuple)): #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 not isinstance(n, (list, tuple)): 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 not isinstance(n, (list, tuple)): 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 isinstance(self.data[0], (list, tuple)): #multi-series doughnut ndata = len(self.data) yir = (yradius / 2.5) / ndata xir = (xradius / 2.5) / ndata 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 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) yr1 = yir + sn * ydr yr = yr1 + ydr xr1 = xir + sn * xdr xr = xr1 + xdr if isinstance(n, (list, tuple)): 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 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 yir = yradius / 2.5 xir = xradius / 2.5 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=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 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( 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"), ) 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.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 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 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) text = self.getSeriesName(i, '') if text: 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) g.add( _addWedgeLabel(self, text, averageAngle, labelX, labelY, sectorStyle)) 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) theLabel = String(labelX, labelY, labels[i]) theLabel.textAnchor = "middle" theLabel.fontSize = sectorStyle.fontSize theLabel.fontName = sectorStyle.fontName theLabel.fillColor = sectorStyle.fontColor g.add(theLabel) return g def draw(self): g = Group() g.add(self.makeSectors()) return g
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